diff --git a/src/core-impl/collections/aggregate/AggregateCollection.cpp b/src/core-impl/collections/aggregate/AggregateCollection.cpp index 458cce8822..aff19486cf 100644 --- a/src/core-impl/collections/aggregate/AggregateCollection.cpp +++ b/src/core-impl/collections/aggregate/AggregateCollection.cpp @@ -1,526 +1,526 @@ /**************************************************************************************** * Copyright (c) 2009 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 . * ****************************************************************************************/ #define DEBUG_PREFIX "AggregateCollection" #include "AggregateCollection.h" #include "core/support/Debug.h" #include "core-impl/collections/aggregate/AggregateMeta.h" #include "core-impl/collections/aggregate/AggregateQueryMaker.h" #include "core-impl/collections/support/CollectionManager.h" #include #include #include using namespace Collections; AggregateCollection::AggregateCollection() : Collections::Collection() { QTimer *timer = new QTimer( this ); timer->setSingleShot( false ); timer->setInterval( 60000 ); //clean up every 60 seconds connect( timer, &QTimer::timeout, this, &AggregateCollection::emptyCache ); timer->start(); } AggregateCollection::~AggregateCollection() { } QString AggregateCollection::prettyName() const { return i18nc( "Name of the virtual collection that merges tracks from all collections", "Aggregate Collection" ); } QIcon AggregateCollection::icon() const { - return QIcon::fromTheme("drive-harddisk"); + return QIcon::fromTheme(QStringLiteral("drive-harddisk")); } bool AggregateCollection::possiblyContainsTrack( const QUrl &url ) const { foreach( Collections::Collection *collection, m_idCollectionMap ) { if( collection->possiblyContainsTrack( url ) ) return true; } return false; } Meta::TrackPtr AggregateCollection::trackForUrl( const QUrl &url ) { foreach( Collections::Collection *collection, m_idCollectionMap ) { Meta::TrackPtr track = collection->trackForUrl( url ); if( track ) { //theoretically we should now query the other collections for the same track //not sure how to do that yet though... return Meta::TrackPtr( getTrack( track ) ); } } return Meta::TrackPtr(); } QueryMaker* AggregateCollection::queryMaker() { QList list; foreach( Collections::Collection *collection, m_idCollectionMap ) { list.append( collection->queryMaker() ); } return new Collections::AggregateQueryMaker( this, list ); } QString AggregateCollection::collectionId() const { // do we need more than one AggregateCollection? return QStringLiteral( "AggregateCollection" ); } void AggregateCollection::addCollection( Collections::Collection *collection, CollectionManager::CollectionStatus status ) { if( !collection ) return; if( !( status & CollectionManager::CollectionViewable ) ) return; m_idCollectionMap.insert( collection->collectionId(), collection ); //TODO Q_EMIT updated(); } void AggregateCollection::removeCollectionById( const QString &collectionId ) { m_idCollectionMap.remove( collectionId ); Q_EMIT updated(); } void AggregateCollection::removeCollection( Collections::Collection *collection ) { m_idCollectionMap.remove( collection->collectionId() ); Q_EMIT updated(); } void AggregateCollection::slotUpdated() { //TODO Q_EMIT updated(); } void AggregateCollection::removeYear( const QString &name ) { m_yearLock.lockForWrite(); m_yearMap.remove( name ); m_yearLock.unlock(); } Meta::AggreagateYear* AggregateCollection::getYear( Meta::YearPtr year ) { m_yearLock.lockForRead(); if( m_yearMap.contains( year->name() ) ) { AmarokSharedPointer aggregateYear = m_yearMap.value( year->name() ); aggregateYear->add( year ); m_yearLock.unlock(); return aggregateYear.data(); } else { m_yearLock.unlock(); m_yearLock.lockForWrite(); //we might create two year instances with the same name here, //which would show some weird behaviour in other places Meta::AggreagateYear *aggregateYear = new Meta::AggreagateYear( this, year ); m_yearMap.insert( year->name(), AmarokSharedPointer( aggregateYear ) ); m_yearLock.unlock(); return aggregateYear; } } void AggregateCollection::setYear( Meta::AggreagateYear *year ) { m_yearLock.lockForWrite(); m_yearMap.insert( year->name(), AmarokSharedPointer( year ) ); m_yearLock.unlock(); } bool AggregateCollection::hasYear( const QString &name ) { QReadLocker locker( &m_yearLock ); return m_yearMap.contains( name ); } void AggregateCollection::removeGenre( const QString &name ) { m_genreLock.lockForWrite(); m_genreMap.remove( name ); m_genreLock.unlock(); } Meta::AggregateGenre* AggregateCollection::getGenre( Meta::GenrePtr genre ) { m_genreLock.lockForRead(); if( m_genreMap.contains( genre->name() ) ) { AmarokSharedPointer aggregateGenre = m_genreMap.value( genre->name() ); aggregateGenre->add( genre ); m_genreLock.unlock(); return aggregateGenre.data(); } else { m_genreLock.unlock(); m_genreLock.lockForWrite(); //we might create two instances with the same name here, //which would show some weird behaviour in other places Meta::AggregateGenre *aggregateGenre = new Meta::AggregateGenre( this, genre ); m_genreMap.insert( genre->name(), AmarokSharedPointer( aggregateGenre ) ); m_genreLock.unlock(); return aggregateGenre; } } void AggregateCollection::setGenre( Meta::AggregateGenre *genre ) { m_genreLock.lockForWrite(); m_genreMap.insert( genre->name(), AmarokSharedPointer( genre ) ); m_genreLock.unlock(); } bool AggregateCollection::hasGenre( const QString &genre ) { QReadLocker locker( &m_genreLock ); return m_genreMap.contains( genre ); } void AggregateCollection::removeComposer( const QString &name ) { m_composerLock.lockForWrite(); m_composerMap.remove( name ); m_composerLock.unlock(); } Meta::AggregateComposer* AggregateCollection::getComposer( Meta::ComposerPtr composer ) { m_composerLock.lockForRead(); if( m_composerMap.contains( composer->name() ) ) { AmarokSharedPointer aggregateComposer = m_composerMap.value( composer->name() ); aggregateComposer->add( composer ); m_composerLock.unlock(); return aggregateComposer.data(); } else { m_composerLock.unlock(); m_composerLock.lockForWrite(); //we might create two instances with the same name here, //which would show some weird behaviour in other places Meta::AggregateComposer *aggregateComposer = new Meta::AggregateComposer( this, composer ); m_composerMap.insert( composer->name(), AmarokSharedPointer( aggregateComposer ) ); m_composerLock.unlock(); return aggregateComposer; } } void AggregateCollection::setComposer( Meta::AggregateComposer *composer ) { m_composerLock.lockForWrite(); m_composerMap.insert( composer->name(), AmarokSharedPointer( composer ) ); m_composerLock.unlock(); } bool AggregateCollection::hasComposer( const QString &name ) { QReadLocker locker( &m_composerLock ); return m_composerMap.contains( name ); } void AggregateCollection::removeArtist( const QString &name ) { m_artistLock.lockForWrite(); m_artistMap.remove( name ); m_artistLock.unlock(); } Meta::AggregateArtist* AggregateCollection::getArtist( Meta::ArtistPtr artist ) { m_artistLock.lockForRead(); if( m_artistMap.contains( artist->name() ) ) { AmarokSharedPointer aggregateArtist = m_artistMap.value( artist->name() ); aggregateArtist->add( artist ); m_artistLock.unlock(); return aggregateArtist.data(); } else { m_artistLock.unlock(); m_artistLock.lockForWrite(); //we might create two instances with the same name here, //which would show some weird behaviour in other places Meta::AggregateArtist *aggregateArtist = new Meta::AggregateArtist( this, artist ); m_artistMap.insert( artist->name(), AmarokSharedPointer( aggregateArtist ) ); m_artistLock.unlock(); return aggregateArtist; } } void AggregateCollection::setArtist( Meta::AggregateArtist *artist ) { m_artistLock.lockForWrite(); m_artistMap.insert( artist->name(), AmarokSharedPointer( artist ) ); m_artistLock.unlock(); } bool AggregateCollection::hasArtist( const QString &artist ) { QReadLocker locker( &m_artistLock ); return m_artistMap.contains( artist ); } void AggregateCollection::removeAlbum( const QString &album, const QString &albumartist ) { Meta::AlbumKey key( album, albumartist ); m_albumLock.lockForWrite(); m_albumMap.remove( key ); m_albumLock.unlock(); } Meta::AggregateAlbum* AggregateCollection::getAlbum( const Meta::AlbumPtr &album ) { Meta::AlbumKey key( album ); m_albumLock.lockForRead(); if( m_albumMap.contains( key ) ) { AmarokSharedPointer aggregateAlbum = m_albumMap.value( key ); aggregateAlbum->add( album ); m_albumLock.unlock(); return aggregateAlbum.data(); } else { m_albumLock.unlock(); m_albumLock.lockForWrite(); //we might create two instances with the same name here, //which would show some weird behaviour in other places Meta::AggregateAlbum *aggregateAlbum = new Meta::AggregateAlbum( this, album ); m_albumMap.insert( key, AmarokSharedPointer( aggregateAlbum ) ); m_albumLock.unlock(); return aggregateAlbum; } } void AggregateCollection::setAlbum( Meta::AggregateAlbum *album ) { m_albumLock.lockForWrite(); m_albumMap.insert( Meta::AlbumKey( Meta::AlbumPtr( album ) ), AmarokSharedPointer( album ) ); m_albumLock.unlock(); } bool AggregateCollection::hasAlbum( const QString &album, const QString &albumArtist ) { QReadLocker locker( &m_albumLock ); return m_albumMap.contains( Meta::AlbumKey( album, albumArtist ) ); } void AggregateCollection::removeTrack( const Meta::TrackKey &key ) { m_trackLock.lockForWrite(); m_trackMap.remove( key ); m_trackLock.unlock(); } Meta::AggregateTrack* AggregateCollection::getTrack( const Meta::TrackPtr &track ) { const Meta::TrackKey key( track ); m_trackLock.lockForRead(); if( m_trackMap.contains( key ) ) { AmarokSharedPointer aggregateTrack = m_trackMap.value( key ); aggregateTrack->add( track ); m_trackLock.unlock(); return aggregateTrack.data(); } else { m_trackLock.unlock(); m_trackLock.lockForWrite(); //we might create two instances with the same name here, //which would show some weird behaviour in other places Meta::AggregateTrack *aggregateTrack = new Meta::AggregateTrack( this, track ); m_trackMap.insert( key, AmarokSharedPointer( aggregateTrack ) ); m_trackLock.unlock(); return aggregateTrack; } } void AggregateCollection::setTrack( Meta::AggregateTrack *track ) { Meta::TrackPtr ptr( track ); const Meta::TrackKey key( ptr ); m_trackLock.lockForWrite(); m_trackMap.insert( key, AmarokSharedPointer( track ) ); m_trackLock.unlock(); } bool AggregateCollection::hasTrack( const Meta::TrackKey &key ) { QReadLocker locker( &m_trackLock ); return m_trackMap.contains( key ); } bool AggregateCollection::hasLabel( const QString &name ) { QReadLocker locker( &m_labelLock ); return m_labelMap.contains( name ); } void AggregateCollection::removeLabel( const QString &name ) { QWriteLocker locker( &m_labelLock ); m_labelMap.remove( name ); } Meta::AggregateLabel* AggregateCollection::getLabel( Meta::LabelPtr label ) { m_labelLock.lockForRead(); if( m_labelMap.contains( label->name() ) ) { AmarokSharedPointer aggregateLabel = m_labelMap.value( label->name() ); aggregateLabel->add( label ); m_labelLock.unlock(); return aggregateLabel.data(); } else { m_labelLock.unlock(); m_labelLock.lockForWrite(); //we might create two year instances with the same name here, //which would show some weird behaviour in other places Meta::AggregateLabel *aggregateLabel = new Meta::AggregateLabel( this, label ); m_labelMap.insert( label->name(), AmarokSharedPointer( aggregateLabel ) ); m_labelLock.unlock(); return aggregateLabel; } } void AggregateCollection::setLabel( Meta::AggregateLabel *label ) { QWriteLocker locker( &m_labelLock ); m_labelMap.insert( label->name(), AmarokSharedPointer( label ) ); } void AggregateCollection::emptyCache() { bool hasTrack, hasAlbum, hasArtist, hasYear, hasGenre, hasComposer, hasLabel; hasTrack = hasAlbum = hasArtist = hasYear = hasGenre = hasComposer = hasLabel = false; //try to avoid possible deadlocks by aborting when we can't get all locks if ( ( hasTrack = m_trackLock.tryLockForWrite() ) && ( hasAlbum = m_albumLock.tryLockForWrite() ) && ( hasArtist = m_artistLock.tryLockForWrite() ) && ( hasYear = m_yearLock.tryLockForWrite() ) && ( hasGenre = m_genreLock.tryLockForWrite() ) && ( hasComposer = m_composerLock.tryLockForWrite() ) && ( hasLabel = m_labelLock.tryLockForWrite() ) ) { //this very simple garbage collector doesn't handle cyclic object graphs //so care has to be taken to make sure that we are not dealing with a cyclic graph //by invalidating the tracks cache on all objects #define foreachInvalidateCache( Type, RealType, x ) \ for( QMutableHashIterator iter(x); iter.hasNext(); ) \ RealType::staticCast( iter.next().value() )->invalidateCache() //elem.count() == 2 is correct because elem is one pointer to the object //and the other is stored in the hash map (except for m_trackMap, where //another reference is stored in m_uidMap #define foreachCollectGarbage( Key, Type, RefCount, x ) \ for( QMutableHashIterator iter(x); iter.hasNext(); ) \ { \ Type elem = iter.next().value(); \ if( elem.count() == RefCount ) \ iter.remove(); \ } foreachCollectGarbage( Meta::TrackKey, AmarokSharedPointer, 2, m_trackMap ) //run before artist so that album artist pointers can be garbage collected foreachCollectGarbage( Meta::AlbumKey, AmarokSharedPointer, 2, m_albumMap ) foreachCollectGarbage( QString, AmarokSharedPointer, 2, m_artistMap ) foreachCollectGarbage( QString, AmarokSharedPointer, 2, m_genreMap ) foreachCollectGarbage( QString, AmarokSharedPointer, 2, m_composerMap ) foreachCollectGarbage( QString, AmarokSharedPointer, 2, m_yearMap ) foreachCollectGarbage( QString, AmarokSharedPointer, 2, m_labelMap ) } //make sure to unlock all necessary locks //important: calling unlock() on an unlocked mutex gives an undefined result //unlocking a mutex locked by another thread results in an error, so be careful if( hasTrack ) m_trackLock.unlock(); if( hasAlbum ) m_albumLock.unlock(); if( hasArtist ) m_artistLock.unlock(); if( hasYear ) m_yearLock.unlock(); if( hasGenre ) m_genreLock.unlock(); if( hasComposer ) m_composerLock.unlock(); if( hasLabel ) m_labelLock.unlock(); } diff --git a/src/core-impl/playlists/types/file/m3u/M3UPlaylist.cpp b/src/core-impl/playlists/types/file/m3u/M3UPlaylist.cpp index facb243e74..2e8e0f03cb 100644 --- a/src/core-impl/playlists/types/file/m3u/M3UPlaylist.cpp +++ b/src/core-impl/playlists/types/file/m3u/M3UPlaylist.cpp @@ -1,115 +1,115 @@ /**************************************************************************************** * Copyright (c) 2007 Bart Cerneels * * * * 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 "M3UPlaylist.h" #include "core/support/Debug.h" #include using namespace Playlists; M3UPlaylist::M3UPlaylist( const QUrl &url, PlaylistProvider *provider ) : PlaylistFile( url, provider ) { } bool M3UPlaylist::loadM3u( QTextStream &stream ) { if( m_tracksLoaded ) return true; m_tracksLoaded = true; int length = -1; QString extinfTitle; do { QString line = stream.readLine(); if( line.startsWith( QLatin1String("#EXTINF") ) ) { const QString extinf = line.section( QLatin1Char(':'), 1 ); bool ok; - length = extinf.section( ',', 0, 0 ).toInt( &ok ); + length = extinf.section( QLatin1Char(','), 0, 0 ).toInt( &ok ); if( !ok ) length = -1; - extinfTitle = extinf.section( ',', 1 ); + extinfTitle = extinf.section( QLatin1Char(','), 1 ); } else if( !line.startsWith( '#' ) && !line.isEmpty() ) { line = line.replace( QLatin1String("\\"), QLatin1String("/") ); QUrl url = getAbsolutePath( QUrl( line ) ); MetaProxy::TrackPtr proxyTrack( new MetaProxy::Track( url ) ); QString artist = extinfTitle.section( QStringLiteral(" - "), 0, 0 ); QString title = extinfTitle.section( QStringLiteral(" - "), 1, 1 ); //if title and artist are saved such as in M3UPlaylist::save() if( !title.isEmpty() && !artist.isEmpty() ) { proxyTrack->setTitle( title ); proxyTrack->setArtist( artist ); } else { proxyTrack->setTitle( extinfTitle ); } proxyTrack->setLength( length ); Meta::TrackPtr track( proxyTrack.data() ); addProxyTrack( track ); } } while( !stream.atEnd() ); //TODO: return false if stream is not readable, empty or has errors return true; } void M3UPlaylist::savePlaylist( QFile &file ) { QTextStream stream( &file ); stream << "#EXTM3U\n"; QList urls; QStringList titles; QList lengths; foreach( const Meta::TrackPtr &track, m_tracks ) { if( !track ) // see BUG: 303056 continue; const QUrl &url = track->playableUrl(); int length = track->length() / 1000; const QString &title = track->name(); const QString &artist = track->artist()->name(); if( !title.isEmpty() && !artist.isEmpty() && length ) { stream << "#EXTINF:"; stream << QString::number( length ); stream << ','; stream << artist << " - " << title; stream << '\n'; } if( url.scheme() == QLatin1String("file") ) stream << trackLocation( track ); else stream << url.url(); stream << "\n"; } } diff --git a/src/core-impl/podcasts/sql/SqlPodcastMeta.cpp b/src/core-impl/podcasts/sql/SqlPodcastMeta.cpp index cc1b590283..874fa7b63e 100644 --- a/src/core-impl/podcasts/sql/SqlPodcastMeta.cpp +++ b/src/core-impl/podcasts/sql/SqlPodcastMeta.cpp @@ -1,954 +1,954 @@ /**************************************************************************************** * Copyright (c) 2008 Bart Cerneels * * * * 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 "SqlPodcastMeta.h" #include "amarokurls/BookmarkMetaActions.h" #include "amarokurls/PlayUrlRunner.h" #include "core/capabilities/ActionsCapability.h" #include #include "core/meta/TrackEditor.h" #include "core/support/Debug.h" #include "core-impl/capabilities/timecode/TimecodeLoadCapability.h" #include "core-impl/capabilities/timecode/TimecodeWriteCapability.h" #include "core-impl/collections/support/CollectionManager.h" #include "core-impl/storage/StorageManager.h" #include "core-impl/meta/proxy/MetaProxy.h" #include "core-impl/meta/file/FileTrackProvider.h" #include "core-impl/podcasts/sql/SqlPodcastProvider.h" #include #include using namespace Podcasts; static FileTrackProvider myFileTrackProvider; // we need it to be available for lookups class TimecodeWriteCapabilityPodcastImpl : public Capabilities::TimecodeWriteCapability { public: TimecodeWriteCapabilityPodcastImpl( Podcasts::PodcastEpisode *episode ) : Capabilities::TimecodeWriteCapability() , m_episode( episode ) {} bool writeTimecode ( qint64 miliseconds ) override { DEBUG_BLOCK return Capabilities::TimecodeWriteCapability::writeTimecode( miliseconds, Meta::TrackPtr::dynamicCast( m_episode ) ); } bool writeAutoTimecode ( qint64 miliseconds ) override { DEBUG_BLOCK return Capabilities::TimecodeWriteCapability::writeAutoTimecode( miliseconds, Meta::TrackPtr::dynamicCast( m_episode ) ); } private: Podcasts::PodcastEpisodePtr m_episode; }; class TimecodeLoadCapabilityPodcastImpl : public Capabilities::TimecodeLoadCapability { public: TimecodeLoadCapabilityPodcastImpl( Podcasts::PodcastEpisode *episode ) : Capabilities::TimecodeLoadCapability() , m_episode( episode ) { DEBUG_BLOCK debug() << "episode: " << m_episode->name(); } bool hasTimecodes() override { if ( loadTimecodes().size() > 0 ) return true; return false; } BookmarkList loadTimecodes() override { DEBUG_BLOCK if ( m_episode && m_episode->playableUrl().isValid() ) { BookmarkList list = PlayUrlRunner::bookmarksFromUrl( m_episode->playableUrl() ); return list; } else return BookmarkList(); } private: Podcasts::PodcastEpisodePtr m_episode; }; Meta::TrackList SqlPodcastEpisode::toTrackList( Podcasts::SqlPodcastEpisodeList episodes ) { Meta::TrackList tracks; foreach( SqlPodcastEpisodePtr sqlEpisode, episodes ) tracks << Meta::TrackPtr::dynamicCast( sqlEpisode ); return tracks; } Podcasts::PodcastEpisodeList SqlPodcastEpisode::toPodcastEpisodeList( SqlPodcastEpisodeList episodes ) { Podcasts::PodcastEpisodeList sqlEpisodes; foreach( SqlPodcastEpisodePtr sqlEpisode, episodes ) sqlEpisodes << Podcasts::PodcastEpisodePtr::dynamicCast( sqlEpisode ); return sqlEpisodes; } SqlPodcastEpisode::SqlPodcastEpisode( const QStringList &result, const SqlPodcastChannelPtr &sqlChannel ) : Podcasts::PodcastEpisode( Podcasts::PodcastChannelPtr::staticCast( sqlChannel ) ) , m_channel( sqlChannel ) { auto sqlStorage = StorageManager::instance()->sqlStorage(); QStringList::ConstIterator iter = result.constBegin(); m_dbId = (*(iter++)).toInt(); m_url = QUrl( *(iter++) ); int channelId = (*(iter++)).toInt(); Q_UNUSED( channelId ); m_localUrl = QUrl( *(iter++) ); m_guid = *(iter++); m_title = *(iter++); m_subtitle = *(iter++); m_sequenceNumber = (*(iter++)).toInt(); m_description = *(iter++); m_mimeType = *(iter++); m_pubDate = QDateTime::fromString( *(iter++), Qt::ISODate ); m_duration = (*(iter++)).toInt(); m_fileSize = (*(iter++)).toInt(); m_isNew = sqlStorage->boolTrue() == (*(iter++)); m_isKeep = sqlStorage->boolTrue() == (*(iter++)); Q_ASSERT_X( iter == result.constEnd(), "SqlPodcastEpisode( PodcastCollection*, QStringList )", "number of expected fields did not match number of actual fields" ); setupLocalFile(); } //TODO: why do PodcastMetaCommon and PodcastEpisode not have an appropriate copy constructor? SqlPodcastEpisode::SqlPodcastEpisode( Podcasts::PodcastEpisodePtr episode ) : Podcasts::PodcastEpisode() , m_dbId( 0 ) , m_isKeep( false ) { m_channel = SqlPodcastChannelPtr::dynamicCast( episode->channel() ); if( !m_channel && episode->channel() ) { debug() << "BUG: creating SqlEpisode but not an sqlChannel!!!"; debug() << episode->channel()->title(); debug() << m_channel->title(); } // PodcastMetaCommon m_title = episode->title(); m_description = episode->description(); m_keywords = episode->keywords(); m_subtitle = episode->subtitle(); m_summary = episode->summary(); m_author = episode->author(); // PodcastEpisode m_guid = episode->guid(); m_url = QUrl( episode->uidUrl() ); m_localUrl = episode->localUrl(); m_mimeType = episode->mimeType(); m_pubDate = episode->pubDate(); m_duration = episode->duration(); m_fileSize = episode->filesize(); m_sequenceNumber = episode->sequenceNumber(); m_isNew = episode->isNew(); // The album, artist, composer, genre and year fields // contain proxy objects with internal references to this. // These proxies are created by Podcasts::PodcastEpisode(), so // these fields don't have to be set here. //commit to the database updateInDb(); setupLocalFile(); } SqlPodcastEpisode::SqlPodcastEpisode( const PodcastChannelPtr &channel, Podcasts::PodcastEpisodePtr episode ) : Podcasts::PodcastEpisode() , m_dbId( 0 ) , m_isKeep( false ) { m_channel = SqlPodcastChannelPtr::dynamicCast( channel ); if( !m_channel && episode->channel() ) { debug() << "BUG: creating SqlEpisode but not an sqlChannel!!!"; debug() << episode->channel()->title(); debug() << m_channel->title(); } // PodcastMetaCommon m_title = episode->title(); m_description = episode->description(); m_keywords = episode->keywords(); m_subtitle = episode->subtitle(); m_summary = episode->summary(); m_author = episode->author(); // PodcastEpisode m_guid = episode->guid(); m_url = QUrl( episode->uidUrl() ); m_localUrl = episode->localUrl(); m_mimeType = episode->mimeType(); m_pubDate = episode->pubDate(); m_duration = episode->duration(); m_fileSize = episode->filesize(); m_sequenceNumber = episode->sequenceNumber(); m_isNew = episode->isNew(); // The album, artist, composer, genre and year fields // contain proxy objects with internal references to this. // These proxies are created by Podcasts::PodcastEpisode(), so // these fields don't have to be set here. //commit to the database updateInDb(); setupLocalFile(); } void SqlPodcastEpisode::setupLocalFile() { if( m_localUrl.isEmpty() || !QFileInfo( m_localUrl.toLocalFile() ).exists() ) return; MetaProxy::TrackPtr proxyTrack( new MetaProxy::Track( m_localUrl, MetaProxy::Track::ManualLookup ) ); m_localFile = Meta::TrackPtr( proxyTrack.data() ); // avoid static_cast /* following won't write to actual file, because MetaProxy::Track hasn't yet looked * up the underlying track. It will just set some cached values. */ writeTagsToFile(); proxyTrack->lookupTrack( &myFileTrackProvider ); } SqlPodcastEpisode::~SqlPodcastEpisode() { } void SqlPodcastEpisode::setNew( bool isNew ) { PodcastEpisode::setNew( isNew ); updateInDb(); } void SqlPodcastEpisode::setKeep( bool isKeep ) { m_isKeep = isKeep; updateInDb(); } void SqlPodcastEpisode::setLocalUrl( const QUrl &url ) { m_localUrl = url; updateInDb(); if( m_localUrl.isEmpty() && !m_localFile.isNull() ) { m_localFile.clear(); notifyObservers(); } else { //if we had a local file previously it should get deleted by the AmarokSharedPointer. m_localFile = new MetaFile::Track( m_localUrl ); if( m_channel->writeTags() ) writeTagsToFile(); } } qint64 SqlPodcastEpisode::length() const { //if downloaded get the duration from the file, else use the value read from the feed if( m_localFile.isNull() ) return m_duration * 1000; return m_localFile->length(); } bool SqlPodcastEpisode::hasCapabilityInterface( Capabilities::Capability::Type type ) const { switch( type ) { case Capabilities::Capability::Actions: case Capabilities::Capability::WriteTimecode: case Capabilities::Capability::LoadTimecode: //only downloaded episodes can be position marked // return !localUrl().isEmpty(); return true; default: return false; } } Capabilities::Capability* SqlPodcastEpisode::createCapabilityInterface( Capabilities::Capability::Type type ) { switch( type ) { case Capabilities::Capability::Actions: { QList< QAction * > actions; actions << new BookmarkCurrentTrackPositionAction( 0 ); return new Capabilities::ActionsCapability( actions ); } case Capabilities::Capability::WriteTimecode: return new TimecodeWriteCapabilityPodcastImpl( this ); case Capabilities::Capability::LoadTimecode: return new TimecodeLoadCapabilityPodcastImpl( this ); default: return 0; } } void SqlPodcastEpisode::finishedPlaying( double playedFraction ) { if( length() <= 0 || playedFraction >= 0.1 ) setNew( false ); PodcastEpisode::finishedPlaying( playedFraction ); } QString SqlPodcastEpisode::name() const { if( m_localFile.isNull() ) return m_title; return m_localFile->name(); } QString SqlPodcastEpisode::prettyName() const { /*for now just do the same as name, but in the future we might want to used a cleaned up string using some sort of regex tag rewrite for podcasts. decapitateString on steroides. */ return name(); } void SqlPodcastEpisode::setTitle( const QString &title ) { m_title = title; Meta::TrackEditorPtr ec = m_localFile ? m_localFile->editor() : Meta::TrackEditorPtr(); if( ec ) ec->setTitle( title ); } Meta::ArtistPtr SqlPodcastEpisode::artist() const { if( m_localFile.isNull() ) return m_artistPtr; return m_localFile->artist(); } Meta::ComposerPtr SqlPodcastEpisode::composer() const { if( m_localFile.isNull() ) return m_composerPtr; return m_localFile->composer(); } Meta::GenrePtr SqlPodcastEpisode::genre() const { if( m_localFile.isNull() ) return m_genrePtr; return m_localFile->genre(); } Meta::YearPtr SqlPodcastEpisode::year() const { if( m_localFile.isNull() ) return m_yearPtr; return m_localFile->year(); } Meta::TrackEditorPtr SqlPodcastEpisode::editor() { if( m_localFile ) return m_localFile->editor(); else return Meta::TrackEditorPtr(); } bool SqlPodcastEpisode::writeTagsToFile() { if( !m_localFile ) return false; Meta::TrackEditorPtr ec = m_localFile->editor(); if( !ec ) return false; debug() << "writing tags for podcast episode " << title() << "to " << m_localUrl.url(); ec->beginUpdate(); ec->setTitle( m_title ); ec->setAlbum( m_channel->title() ); ec->setArtist( m_channel->author() ); ec->setGenre( i18n( "Podcast" ) ); ec->setYear( m_pubDate.date().year() ); ec->endUpdate(); notifyObservers(); return true; } void SqlPodcastEpisode::updateInDb() { auto sqlStorage = StorageManager::instance()->sqlStorage(); QString boolTrue = sqlStorage->boolTrue(); QString boolFalse = sqlStorage->boolFalse(); #define escape(x) sqlStorage->escape(x) QString command; QTextStream stream( &command ); if( m_dbId ) { stream << "UPDATE podcastepisodes "; stream << "SET url='"; stream << escape(m_url.url()); stream << "', channel="; stream << m_channel->dbId(); stream << ", localurl='"; stream << escape(m_localUrl.url()); stream << "', guid='"; stream << escape(m_guid); stream << "', title='"; stream << escape(m_title); stream << "', subtitle='"; stream << escape(m_subtitle); stream << "', sequencenumber="; stream << m_sequenceNumber; stream << ", description='"; stream << escape(m_description); stream << "', mimetype='"; stream << escape(m_mimeType); stream << "', pubdate='"; stream << escape(m_pubDate.toString(Qt::ISODate)); stream << "', duration="; stream << m_duration; stream << ", filesize="; stream << m_fileSize; stream << ", isnew="; stream << (isNew() ? boolTrue : boolFalse); stream << ", iskeep="; stream << (isKeep() ? boolTrue : boolFalse); stream << " WHERE id="; stream << m_dbId; stream << ";"; sqlStorage->query( command ); } else { stream << "INSERT INTO podcastepisodes ("; stream << "url,channel,localurl,guid,title,subtitle,sequencenumber,description,"; stream << "mimetype,pubdate,duration,filesize,isnew,iskeep) "; stream << "VALUES ( '"; stream << escape(m_url.url()) << "', "; stream << m_channel->dbId() << ", '"; stream << escape(m_localUrl.url()) << "', '"; stream << escape(m_guid) << "', '"; stream << escape(m_title) << "', '"; stream << escape(m_subtitle) << "', "; stream << m_sequenceNumber << ", '"; stream << escape(m_description) << "', '"; stream << escape(m_mimeType) << "', '"; stream << escape(m_pubDate.toString(Qt::ISODate)) << "', "; stream << m_duration << ", "; stream << m_fileSize << ", "; stream << (isNew() ? boolTrue : boolFalse) << ", "; stream << (isKeep() ? boolTrue : boolFalse); stream << ");"; m_dbId = sqlStorage->insert( command, QStringLiteral("podcastepisodes") ); } } void SqlPodcastEpisode::deleteFromDb() { auto sqlStorage = StorageManager::instance()->sqlStorage(); sqlStorage->query( QStringLiteral( "DELETE FROM podcastepisodes WHERE id = %1;" ).arg( dbId() ) ); } Playlists::PlaylistPtr SqlPodcastChannel::toPlaylistPtr( const SqlPodcastChannelPtr &sqlChannel ) { Playlists::PlaylistPtr playlist = Playlists::PlaylistPtr::dynamicCast( sqlChannel ); return playlist; } SqlPodcastChannelPtr SqlPodcastChannel::fromPlaylistPtr( const Playlists::PlaylistPtr &playlist ) { SqlPodcastChannelPtr sqlChannel = SqlPodcastChannelPtr::dynamicCast( playlist ); return sqlChannel; } SqlPodcastChannel::SqlPodcastChannel( SqlPodcastProvider *provider, const QStringList &result ) : Podcasts::PodcastChannel() , m_episodesLoaded( false ) , m_trackCacheIsValid( false ) , m_provider( provider ) { auto sqlStorage = StorageManager::instance()->sqlStorage(); QStringList::ConstIterator iter = result.constBegin(); m_dbId = (*(iter++)).toInt(); m_url = QUrl( *(iter++) ); m_title = *(iter++); m_webLink = QUrl::fromUserInput(*(iter++)); m_imageUrl = QUrl::fromUserInput(*(iter++)); m_description = *(iter++); m_copyright = *(iter++); m_directory = QUrl( *(iter++) ); - m_labels = QStringList( QString( *(iter++) ).split( ',', QString::SkipEmptyParts ) ); + m_labels = QStringList( QString( *(iter++) ).split( QLatin1Char(','), QString::SkipEmptyParts ) ); m_subscribeDate = QDate::fromString( *(iter++) ); m_autoScan = sqlStorage->boolTrue() == *(iter++); m_fetchType = (*(iter++)).toInt() == DownloadWhenAvailable ? DownloadWhenAvailable : StreamOrDownloadOnDemand; m_purge = sqlStorage->boolTrue() == *(iter++); m_purgeCount = (*(iter++)).toInt(); m_writeTags = sqlStorage->boolTrue() == *(iter++); m_filenameLayout = *(iter++); } SqlPodcastChannel::SqlPodcastChannel( Podcasts::SqlPodcastProvider *provider, Podcasts::PodcastChannelPtr channel ) : Podcasts::PodcastChannel() , m_dbId( 0 ) , m_trackCacheIsValid( false ) , m_provider( provider ) , m_filenameLayout( QStringLiteral("%default%") ) { // PodcastMetaCommon m_title = channel->title(); m_description = channel->description(); m_keywords = channel->keywords(); m_subtitle = channel->subtitle(); m_summary = channel->summary(); m_author = channel->author(); // PodcastChannel m_url = channel->url(); m_webLink = channel->webLink(); m_imageUrl = channel->imageUrl(); m_labels = channel->labels(); m_subscribeDate = channel->subscribeDate(); m_copyright = channel->copyright(); if( channel->hasImage() ) m_image = channel->image(); //Default Settings m_directory = QUrl( m_provider->baseDownloadDir() ); m_directory = m_directory.adjusted(QUrl::StripTrailingSlash); m_directory.setPath( QDir::toNativeSeparators(m_directory.path() + QLatin1Char('/') + Amarok::vfatPath( m_title )) ); m_autoScan = true; m_fetchType = StreamOrDownloadOnDemand; m_purge = false; m_purgeCount = 10; m_writeTags = true; updateInDb(); foreach( Podcasts::PodcastEpisodePtr episode, channel->episodes() ) { episode->setChannel( PodcastChannelPtr( this ) ); SqlPodcastEpisode *sqlEpisode = new SqlPodcastEpisode( episode ); m_episodes << SqlPodcastEpisodePtr( sqlEpisode ); } m_episodesLoaded = true; } int SqlPodcastChannel::trackCount() const { if( m_episodesLoaded ) return m_episodes.count(); else return -1; } void SqlPodcastChannel::triggerTrackLoad() { if( !m_episodesLoaded ) loadEpisodes(); notifyObserversTracksLoaded(); } Playlists::PlaylistProvider * SqlPodcastChannel::provider() const { return dynamic_cast( m_provider ); } QStringList SqlPodcastChannel::groups() { return m_labels; } void SqlPodcastChannel::setGroups( const QStringList &groups ) { m_labels = groups; } QUrl SqlPodcastChannel::uidUrl() const { return QUrl( QStringLiteral( "amarok-sqlpodcastuid://%1").arg( m_dbId ) ); } SqlPodcastChannel::~SqlPodcastChannel() { m_episodes.clear(); } void SqlPodcastChannel::setTitle( const QString &title ) { /* also change the savelocation if a title is not set yet. This is a special condition that can happen when first fetching a podcast feed */ if( m_title.isEmpty() ) { m_directory = m_directory.adjusted(QUrl::StripTrailingSlash); m_directory.setPath( QDir::toNativeSeparators(m_directory.path() + QLatin1Char('/') + Amarok::vfatPath( title )) ); } m_title = title; } Podcasts::PodcastEpisodeList SqlPodcastChannel::episodes() const { return SqlPodcastEpisode::toPodcastEpisodeList( m_episodes ); } void SqlPodcastChannel::setImage( const QImage &image ) { DEBUG_BLOCK m_image = image; } void SqlPodcastChannel::setImageUrl( const QUrl &imageUrl ) { DEBUG_BLOCK debug() << imageUrl; m_imageUrl = imageUrl; if( imageUrl.isLocalFile() ) { m_image = QImage( imageUrl.path() ); return; } debug() << "Image is remote, handled by podcastImageFetcher."; } Podcasts::PodcastEpisodePtr SqlPodcastChannel::addEpisode(const PodcastEpisodePtr &episode ) { if( !m_provider ) return PodcastEpisodePtr(); QUrl checkUrl; //searched in the database for guid or enclosure url if( !episode->guid().isEmpty() ) checkUrl = QUrl::fromUserInput(episode->guid()); else if( !episode->uidUrl().isEmpty() ) checkUrl = QUrl::fromUserInput(episode->uidUrl()); else return PodcastEpisodePtr(); //noting to check for if( m_provider->possiblyContainsTrack( checkUrl ) ) return PodcastEpisodePtr::dynamicCast( m_provider->trackForUrl( QUrl::fromUserInput(episode->guid()) ) ); //force episodes load. if( !m_episodesLoaded ) loadEpisodes(); SqlPodcastEpisodePtr sqlEpisode; if (SqlPodcastEpisodePtr::dynamicCast( episode )) sqlEpisode = SqlPodcastEpisodePtr( new SqlPodcastEpisode( episode ) ); else sqlEpisode = SqlPodcastEpisodePtr( new SqlPodcastEpisode( PodcastChannelPtr(this) , episode ) ); //episodes are sorted on pubDate high to low int i; for( i = 0; i < m_episodes.count() ; i++ ) { if( sqlEpisode->pubDate() > m_episodes[i]->pubDate() ) { m_episodes.insert( i, sqlEpisode ); break; } } //insert in case the list is empty or at the end of the list if( i == m_episodes.count() ) m_episodes << sqlEpisode; notifyObserversTrackAdded( Meta::TrackPtr::dynamicCast( sqlEpisode ), i ); applyPurge(); m_trackCacheIsValid = false; return PodcastEpisodePtr::dynamicCast( sqlEpisode ); } void SqlPodcastChannel::applyPurge() { DEBUG_BLOCK if( !hasPurge() ) return; if( m_episodes.count() > purgeCount() ) { int purgeIndex = 0; foreach( SqlPodcastEpisodePtr episode, m_episodes ) { if ( !episode->isKeep() ) { if( purgeIndex >= purgeCount() ) { m_provider->deleteDownloadedEpisode( episode ); m_episodes.removeOne( episode ); } else purgeIndex++; } } m_trackCacheIsValid = false; } } void SqlPodcastChannel::updateInDb() { auto sqlStorage = StorageManager::instance()->sqlStorage(); QString boolTrue = sqlStorage->boolTrue(); QString boolFalse = sqlStorage->boolFalse(); #define escape(x) sqlStorage->escape(x) QString command; QTextStream stream( &command ); if( m_dbId ) { stream << "UPDATE podcastchannels "; stream << "SET url='"; stream << escape(m_url.url()); stream << "', title='"; stream << escape(m_title); stream << "', weblink='"; stream << escape(m_webLink.url()); stream << "', image='"; stream << escape(m_imageUrl.url()); stream << "', description='"; stream << escape(m_description); stream << "', copyright='"; stream << escape(m_copyright); stream << "', directory='"; stream << escape(m_directory.url()); stream << "', labels='"; - stream << escape(m_labels.join( "," )); + stream << escape(m_labels.join( QLatin1Char(',') )); stream << "', subscribedate='"; stream << escape(m_subscribeDate.toString()); stream << "', autoscan="; stream << (m_autoScan ? boolTrue : boolFalse); stream << ", fetchtype="; stream << QString::number(m_fetchType); stream << ", haspurge="; stream << (m_purge ? boolTrue : boolFalse); stream << ", purgecount="; stream << QString::number(m_purgeCount); stream << ", writetags="; stream << (m_writeTags ? boolTrue : boolFalse); stream << ", filenamelayout='"; stream << escape(m_filenameLayout); stream << "' WHERE id="; stream << m_dbId; stream << ";"; debug() << command; sqlStorage->query( command ); } else { stream << "INSERT INTO podcastchannels("; stream << "url,title,weblink,image,description,copyright,directory,labels,"; stream << "subscribedate,autoscan,fetchtype,haspurge,purgecount,writetags,filenamelayout) "; stream << "VALUES ( '"; stream << escape(m_url.url()) << "', '"; stream << escape(m_title) << "', '"; stream << escape(m_webLink.url()) << "', '"; stream << escape(m_imageUrl.url()) << "', '"; stream << escape(m_description) << "', '"; stream << escape(m_copyright) << "', '"; stream << escape(m_directory.url()) << "', '"; - stream << escape(m_labels.join( "," )) << "', '"; + stream << escape(m_labels.join( QLatin1Char(',') )) << "', '"; stream << escape(m_subscribeDate.toString()) << "', "; stream << (m_autoScan ? boolTrue : boolFalse) << ", "; stream << QString::number(m_fetchType) << ", "; stream << (m_purge ? boolTrue : boolFalse) << ", "; stream << QString::number(m_purgeCount) << ", "; stream << (m_writeTags ? boolTrue : boolFalse) << ", '"; stream << escape(m_filenameLayout); stream << "');"; debug() << command; m_dbId = sqlStorage->insert( command, QStringLiteral("podcastchannels") ); } } void SqlPodcastChannel::deleteFromDb() { auto sqlStorage = StorageManager::instance()->sqlStorage(); foreach( SqlPodcastEpisodePtr sqlEpisode, m_episodes ) { sqlEpisode->deleteFromDb(); m_episodes.removeOne( sqlEpisode ); } m_trackCacheIsValid = false; sqlStorage->query( QStringLiteral( "DELETE FROM podcastchannels WHERE id = %1;" ).arg( dbId() ) ); } void SqlPodcastChannel::loadEpisodes() { m_episodes.clear(); auto sqlStorage = StorageManager::instance()->sqlStorage(); //If purge is enabled we must limit the number of results QString command; int rowLength = 15; //If purge is enabled we must limit the number of results, though there are some files //the user want to be shown even if there is no more slot if( hasPurge() ) { command = QString( "(SELECT id, url, channel, localurl, guid, " "title, subtitle, sequencenumber, description, mimetype, pubdate, " "duration, filesize, isnew, iskeep FROM podcastepisodes WHERE channel = %1 " "AND iskeep IS FALSE ORDER BY pubdate DESC LIMIT " + QString::number( purgeCount() ) + ") " "UNION " "(SELECT id, url, channel, localurl, guid, " "title, subtitle, sequencenumber, description, mimetype, pubdate, " "duration, filesize, isnew, iskeep FROM podcastepisodes WHERE channel = %1 " "AND iskeep IS TRUE) " "ORDER BY pubdate DESC;" ); } else { command = QString( "SELECT id, url, channel, localurl, guid, " "title, subtitle, sequencenumber, description, mimetype, pubdate, " "duration, filesize, isnew, iskeep FROM podcastepisodes WHERE channel = %1 " "ORDER BY pubdate DESC;" ); } QStringList results = sqlStorage->query( command.arg( m_dbId ) ); for( int i = 0; i < results.size(); i += rowLength ) { QStringList episodesResult = results.mid( i, rowLength ); SqlPodcastEpisodePtr sqlEpisode = SqlPodcastEpisodePtr( new SqlPodcastEpisode( episodesResult, SqlPodcastChannelPtr( this ) ) ); m_episodes << sqlEpisode; } m_episodesLoaded = true; m_trackCacheIsValid = false; } Meta::TrackList Podcasts::SqlPodcastChannel::tracks() { if ( !m_trackCacheIsValid ) { m_episodesAsTracksCache = Podcasts::SqlPodcastEpisode::toTrackList( m_episodes ); m_trackCacheIsValid = true; } return m_episodesAsTracksCache; } void Podcasts::SqlPodcastChannel::syncTrackStatus(int position, const Meta::TrackPtr &otherTrack ) { Q_UNUSED( position ); Podcasts::PodcastEpisodePtr master = Podcasts::PodcastEpisodePtr::dynamicCast( otherTrack ); if ( master ) { this->setName( master->channel()->name() ); this->setTitle( master->channel()->title() ); this->setUrl( master->channel()->url() ); } } void Podcasts::SqlPodcastChannel::addTrack( const Meta::TrackPtr &track, int position ) { Q_UNUSED( position ); addEpisode( Podcasts::PodcastEpisodePtr::dynamicCast( track ) ); notifyObserversTrackAdded( track, position ); } diff --git a/src/core-impl/podcasts/sql/SqlPodcastProvider.h b/src/core-impl/podcasts/sql/SqlPodcastProvider.h index 9e7418076c..429b746022 100644 --- a/src/core-impl/podcasts/sql/SqlPodcastProvider.h +++ b/src/core-impl/podcasts/sql/SqlPodcastProvider.h @@ -1,206 +1,206 @@ /**************************************************************************************** * Copyright (c) 2007 Bart Cerneels * * * * 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 . * ****************************************************************************************/ #ifndef SQLPODCASTPROVIDER_H #define SQLPODCASTPROVIDER_H #include "core/podcasts/PodcastProvider.h" #include "core/podcasts/PodcastReader.h" #include "SqlPodcastMeta.h" #include #include class PodcastImageFetcher; class QDialog; class QUrl; class PodcastReader; class SqlStorage; class QTimer; namespace Ui { class SqlPodcastProviderSettingsWidget; } namespace Podcasts { /** @author Bart Cerneels */ class AMAROK_EXPORT SqlPodcastProvider : public Podcasts::PodcastProvider { Q_OBJECT public: SqlPodcastProvider(); - virtual ~SqlPodcastProvider(); + ~SqlPodcastProvider() override; //TrackProvider methods bool possiblyContainsTrack( const QUrl &url ) const override; Meta::TrackPtr trackForUrl( const QUrl &url ) override; //PlaylistProvider methods QString prettyName() const override { return i18n("Local Podcasts"); } - QIcon icon() const override { return QIcon::fromTheme( "server-database" ); } + QIcon icon() const override { return QIcon::fromTheme( QStringLiteral("server-database") ); } Playlists::PlaylistList playlists() override; //PlaylistProvider methods QActionList providerActions() override; QActionList playlistActions( const Playlists::PlaylistList &playlists ) override; QActionList trackActions( const QMultiHash &playlistTracks ) override; //PodcastProvider methods Podcasts::PodcastEpisodePtr episodeForGuid( const QString &guid ) override; void addPodcast( const QUrl &url ) override; Podcasts::PodcastChannelPtr addChannel( const Podcasts::PodcastChannelPtr &channel ) override; Podcasts::PodcastEpisodePtr addEpisode( Podcasts::PodcastEpisodePtr episode ) override; Podcasts::PodcastChannelList channels() override; void completePodcastDownloads() override; //SqlPodcastProvider specific methods virtual Podcasts::SqlPodcastChannelPtr podcastChannelForId( int podcastChannelDbId ); virtual QUrl baseDownloadDir() const { return m_baseDownloadDir; } public Q_SLOTS: void updateAll() override; void downloadEpisode( const Podcasts::PodcastEpisodePtr &episode ); void deleteDownloadedEpisode( const Podcasts::PodcastEpisodePtr &episode ); void slotReadResult( PodcastReader *podcastReader ); void downloadEpisode( Podcasts::SqlPodcastEpisodePtr episode ); void deleteDownloadedEpisode( Podcasts::SqlPodcastEpisodePtr episode ); private Q_SLOTS: void downloadResult( KJob * ); void addData( KIO::Job *job, const QByteArray & data ); void redirected( KIO::Job *, const QUrl& ); void autoUpdate(); void slotDeleteDownloadedEpisodes(); void slotDownloadEpisodes(); void slotSetKeep(); void slotConfigureChannel(); void slotRemoveChannels(); void slotUpdateChannels(); void slotDownloadProgress( KJob *job, unsigned long percent ); void slotWriteTagsToFiles(); void slotConfigChanged(); void slotExportOpml(); Q_SIGNALS: void totalPodcastDownloadProgress( int progress ); //SqlPodcastProvider signals void episodeDownloaded( Podcasts::PodcastEpisodePtr ); void episodeDeleted( Podcasts::PodcastEpisodePtr ); private Q_SLOTS: void channelImageReady( Podcasts::PodcastChannelPtr, const QImage &); void podcastImageFetcherDone( PodcastImageFetcher * ); void slotConfigureProvider(); void slotStatusBarNewProgressOperation( KIO::TransferJob * job, const QString &description, Podcasts::PodcastReader* reader ); void slotStatusBarSorryMessage( const QString &message ); void slotOpmlWriterDone( int result ); private: void startTimer(); void configureProvider(); void configureChannel( Podcasts::SqlPodcastChannelPtr channel ); void updateSqlChannel( Podcasts::SqlPodcastChannelPtr channel ); /** creates all the necessary tables, indexes etc. for the database */ void createTables() const; void loadPodcasts(); /** @arg string: a url, localUrl or guid in string form */ Podcasts::SqlPodcastEpisodePtr sqlEpisodeForString( const QString &string ); void updateDatabase( int fromVersion, int toVersion ); void fetchImage( const Podcasts::SqlPodcastChannelPtr &channel ); /** shows a modal dialog asking the user if he really wants to unsubscribe and if he wants to keep the podcast media */ QPair confirmUnsubscribe( Podcasts::SqlPodcastChannelPtr channel ); /** remove the episodes in the list from the filesystem */ void deleteDownloadedEpisodes( Podcasts::SqlPodcastEpisodeList &episodes ); void moveDownloadedEpisodes( Podcasts::SqlPodcastChannelPtr channel ); /** Removes a podcast from the list. Will ask for confirmation to delete the episodes * as well */ void removeSubscription( Podcasts::SqlPodcastChannelPtr channel ); void subscribe( const QUrl &url ); QFile* createTmpFile ( Podcasts::SqlPodcastEpisodePtr sqlEpisode ); void cleanupDownload( KJob *job, bool downloadFailed ); /** returns true if the file that is downloaded by 'job' is already locally available */ bool checkEnclosureLocallyAvailable( KIO::Job *job ); Podcasts::SqlPodcastChannelList m_channels; QTimer *m_updateTimer; int m_autoUpdateInterval; //interval between autoupdate attempts in minutes unsigned int m_updatingChannels; unsigned int m_maxConcurrentUpdates; Podcasts::SqlPodcastChannelList m_updateQueue; QList m_subscribeQueue; struct PodcastEpisodeDownload { Podcasts::SqlPodcastEpisodePtr episode; QFile *tmpFile; QString fileName; bool finalNameReady; }; QHash m_downloadJobMap; Podcasts::SqlPodcastEpisodeList m_downloadQueue; int m_maxConcurrentDownloads; int m_completedDownloads; QUrl m_baseDownloadDir; QDialog *m_providerSettingsDialog; Ui::SqlPodcastProviderSettingsWidget *m_providerSettingsWidget; QList m_providerActions; QAction *m_configureChannelAction; //Configure a Channel QAction *m_deleteAction; //delete a downloaded Episode QAction *m_downloadAction; QAction *m_keepAction; QAction *m_removeAction; //remove a subscription QAction *m_updateAction; QAction *m_writeTagsAction; //write feed information to downloaded file PodcastImageFetcher *m_podcastImageFetcher; }; } //namespace Podcasts #endif diff --git a/src/core/transcoding/formats/TranscodingMp3Format.cpp b/src/core/transcoding/formats/TranscodingMp3Format.cpp index a2d1461ade..47ad20fced 100644 --- a/src/core/transcoding/formats/TranscodingMp3Format.cpp +++ b/src/core/transcoding/formats/TranscodingMp3Format.cpp @@ -1,108 +1,108 @@ /**************************************************************************************** * Copyright (c) 2010 Téo Mrnjavac * * * * 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 "TranscodingMp3Format.h" #include using namespace Transcoding; Mp3Format::Mp3Format() { m_encoder = MP3; m_fileExtension = QStringLiteral("mp3"); const QString description1 = i18n( "The bitrate is a measure of the quantity of data used to represent a " "second of the audio track.
The MP3 encoder used by Amarok supports " "a variable bitrate (VBR) " "setting, which means that the bitrate value fluctuates along the track " "based on the complexity of the audio content. More complex intervals of " "data are encoded with a higher bitrate than less complex ones; this " "approach yields overall better quality and a smaller file than having a " "constant bitrate throughout the track.
" "For this reason, the bitrate measure in this slider is just an estimate " "of the average bitrate of the encoded track.
" "160kb/s is a good choice for music listening on a portable player.
" "Anything below 120kb/s might be unsatisfactory for music and anything above " "205kb/s is probably overkill."); QStringList valueLabels; QByteArray vbr = "VBR ~%1kb/s"; valueLabels << i18n( vbr, 80 ) << i18n( vbr, 100 ) << i18n( vbr, 120 ) << i18n( vbr, 140 ) << i18n( vbr, 160 ) << i18n( vbr, 175 ) << i18n( vbr, 190 ) << i18n( vbr, 205 ) << i18n( vbr, 220 ) << i18n( vbr, 240 ); m_propertyList << Property::Tradeoff( "quality", i18n( "Expected average bitrate for variable bitrate encoding" ), description1, i18n( "Smaller file" ), i18n( "Better sound quality" ), valueLabels, 5 ); } QString Mp3Format::prettyName() const { return i18n( "MP3" ); } QString Mp3Format::description() const { return i18nc( "Feel free to redirect the english Wikipedia link to a local version, if " "it exists.", "MPEG Audio Layer 3 (MP3) is " "a patented digital audio codec using a form of lossy data compression." "
In spite of its shortcomings, it is a common format for consumer " "audio storage, and is widely supported on portable music players." ); } QIcon Mp3Format::icon() const { - return QIcon::fromTheme( "audio-x-generic" ); //TODO: get a *real* icon! + return QIcon::fromTheme( QStringLiteral("audio-x-generic") ); //TODO: get a *real* icon! } QStringList Mp3Format::ffmpegParameters( const Configuration &configuration ) const { QStringList parameters; parameters << QStringLiteral("-acodec") << QStringLiteral("libmp3lame"); foreach( const Property &property, m_propertyList ) { if( !configuration.property( property.name() ).isNull() && configuration.property( property.name() ).type() == property.variantType() ) { if( property.name() == "quality" ) { int ffmpegQuality = qAbs( configuration.property( "quality" ).toInt() - 9 ); parameters << QStringLiteral("-aq") << QString::number( ffmpegQuality ); } } } parameters << QStringLiteral("-vcodec") << QStringLiteral("copy"); // keep album art unchanged return parameters; } bool Mp3Format::verifyAvailability( const QString &ffmpegOutput ) const { return ffmpegOutput.contains( QRegExp( QStringLiteral("^ .EA... mp3 +.*libmp3lame") ) ); } diff --git a/src/playlist/layouts/LayoutManager.cpp b/src/playlist/layouts/LayoutManager.cpp index 837e9863b3..1e13b78db8 100644 --- a/src/playlist/layouts/LayoutManager.cpp +++ b/src/playlist/layouts/LayoutManager.cpp @@ -1,497 +1,497 @@ /**************************************************************************************** * Copyright (c) 2008-2009 Nikolaj Hald Nielsen * * Copyright (c) 2009 Seb Ruiz * * Copyright (c) 2010 Oleksandr Khayrullin * * * * 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 "LayoutManager.h" #include "core/support/Amarok.h" #include "core/support/Components.h" #include "core/support/Debug.h" #include "core/logger/Logger.h" #include "playlist/PlaylistDefines.h" #include "playlist/PlaylistModelStack.h" #include #include #include #include #include #include #include namespace Playlist { static const QString PREVIEW_LAYOUT = QStringLiteral("%%PREVIEW%%"); LayoutManager* LayoutManager::s_instance = nullptr; LayoutManager* LayoutManager::instance() { if( !s_instance ) s_instance = new LayoutManager(); return s_instance; } LayoutManager::LayoutManager() : QObject() { DEBUG_BLOCK loadDefaultLayouts(); loadUserLayouts(); orderLayouts(); KConfigGroup config = Amarok::config(QStringLiteral("Playlist Layout")); m_activeLayout = config.readEntry( "CurrentLayout", "Default" ); if( !layouts().contains( m_activeLayout ) ) m_activeLayout = QStringLiteral("Default"); Playlist::ModelStack::instance()->groupingProxy()->setGroupingCategory( activeLayout().groupBy() ); } QStringList LayoutManager::layouts() const { return m_layoutNames; } void LayoutManager::setActiveLayout( const QString &layout ) { m_activeLayout = layout; Amarok::config( QStringLiteral("Playlist Layout") ).writeEntry( "CurrentLayout", m_activeLayout ); Q_EMIT( activeLayoutChanged() ); //Change the grouping style to that of this layout. Playlist::ModelStack::instance()->groupingProxy()->setGroupingCategory( activeLayout().groupBy() ); } void LayoutManager::setPreviewLayout( const PlaylistLayout &layout ) { DEBUG_BLOCK m_activeLayout = PREVIEW_LAYOUT; m_previewLayout = layout; Q_EMIT( activeLayoutChanged() ); //Change the grouping style to that of this layout. Playlist::ModelStack::instance()->groupingProxy()->setGroupingCategory( activeLayout().groupBy() ); } void LayoutManager::updateCurrentLayout( const PlaylistLayout &layout ) { //Do not store preview layouts. if ( m_activeLayout == PREVIEW_LAYOUT ) return; if ( m_layouts.value( m_activeLayout ).isEditable() ) { addUserLayout( m_activeLayout, layout ); setActiveLayout( m_activeLayout ); } else { //create a writable copy of this layout. (Copy on Write) QString newLayoutName = i18n( "copy of %1", m_activeLayout ); QString orgCopyName = newLayoutName; int copyNumber = 1; QStringList existingLayouts = LayoutManager::instance()->layouts(); while( existingLayouts.contains( newLayoutName ) ) { copyNumber++; newLayoutName = i18nc( "adds a copy number to a generated name if the name already exists, for instance 'copy of Foo 2' if 'copy of Foo' is taken", "%1 %2", orgCopyName, copyNumber ); } Amarok::Logger::longMessage( i18n( "Current layout '%1' is read only. " \ "Creating a new layout '%2' with your changes and setting this as active", m_activeLayout, newLayoutName ) ); addUserLayout( newLayoutName, layout ); setActiveLayout( newLayoutName ); } } PlaylistLayout LayoutManager::activeLayout() const { if ( m_activeLayout == PREVIEW_LAYOUT ) return m_previewLayout; return m_layouts.value( m_activeLayout ); } void LayoutManager::loadUserLayouts() { QDir layoutsDir = QDir( Amarok::saveLocation( QStringLiteral("playlist_layouts/") ) ); layoutsDir.setSorting( QDir::Name ); QStringList filters; filters << QStringLiteral("*.xml") << QStringLiteral("*.XML"); layoutsDir.setNameFilters( filters ); layoutsDir.setSorting( QDir::Name ); QFileInfoList list = layoutsDir.entryInfoList(); for ( int i = 0; i < list.size(); ++i ) { QFileInfo fileInfo = list.at(i); loadLayouts( layoutsDir.filePath( fileInfo.fileName() ), true ); } } void LayoutManager::loadDefaultLayouts() { const QString dataLocation = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("amarok/data"), QStandardPaths::LocateDirectory); - QString configFile = dataLocation + "/DefaultPlaylistLayouts.xml"; + QString configFile = dataLocation + QStringLiteral("/DefaultPlaylistLayouts.xml"); loadLayouts( configFile, false ); } void LayoutManager::loadLayouts( const QString &fileName, bool user ) { DEBUG_BLOCK QDomDocument doc( QStringLiteral("layouts") ); if ( !QFile::exists( fileName ) ) { debug() << "file " << fileName << "does not exist"; return; } QFile *file = new QFile( fileName ); if( !file || !file->open( QIODevice::ReadOnly ) ) { debug() << "error reading file " << fileName; return; } if ( !doc.setContent( file ) ) { debug() << "error parsing file " << fileName; file->close(); return ; } file->close(); delete file; QDomElement layouts_element = doc.firstChildElement( QStringLiteral("playlist_layouts") ); QDomNodeList layouts = layouts_element.elementsByTagName(QStringLiteral("layout")); int index = 0; while ( index < layouts.size() ) { QDomNode layout = layouts.item( index ); index++; QString layoutName = layout.toElement().attribute( QStringLiteral("name"), QLatin1String("") ); debug() << "loading layout " << layoutName; PlaylistLayout currentLayout; currentLayout.setEditable( user ); currentLayout.setInlineControls( layout.toElement().attribute( QStringLiteral("inline_controls"), QStringLiteral("false") ).compare( QLatin1String("true"), Qt::CaseInsensitive ) == 0 ); currentLayout.setTooltips( layout.toElement().attribute( QStringLiteral("tooltips"), QStringLiteral("false") ).compare( QLatin1String("true"), Qt::CaseInsensitive ) == 0 ); //For backwards compatibility, if a grouping is not set in the XML file assume "group by album" (which was previously the default) currentLayout.setGroupBy( layout.toElement().attribute( QStringLiteral("group_by"), QStringLiteral("Album") ) ); debug() << "grouping mode is: " << layout.toElement().attribute( QStringLiteral("group_by"), QStringLiteral("Album") ); currentLayout.setLayoutForPart( PlaylistLayout::Head, parseItemConfig( layout.toElement().firstChildElement( QStringLiteral("group_head") ) ) ); currentLayout.setLayoutForPart( PlaylistLayout::StandardBody, parseItemConfig( layout.toElement().firstChildElement( QStringLiteral("group_body") ) ) ); QDomElement variousArtistsXML = layout.toElement().firstChildElement( QStringLiteral("group_variousArtistsBody") ); if ( !variousArtistsXML.isNull() ) currentLayout.setLayoutForPart( PlaylistLayout::VariousArtistsBody, parseItemConfig( variousArtistsXML ) ); else // Handle old custom layout XMLs currentLayout.setLayoutForPart( PlaylistLayout::VariousArtistsBody, parseItemConfig( layout.toElement().firstChildElement( QStringLiteral("group_body") ) ) ); currentLayout.setLayoutForPart( PlaylistLayout::Single, parseItemConfig( layout.toElement().firstChildElement( QStringLiteral("single_track") ) ) ); if ( !layoutName.isEmpty() ) m_layouts.insert( layoutName, currentLayout ); } } LayoutItemConfig LayoutManager::parseItemConfig( const QDomElement &elem ) const { const bool showCover = ( elem.attribute( QStringLiteral("show_cover"), QStringLiteral("false") ).compare( QLatin1String("true"), Qt::CaseInsensitive ) == 0 ); const int activeIndicatorRow = elem.attribute( QStringLiteral("active_indicator_row"), QStringLiteral("0") ).toInt(); LayoutItemConfig config; config.setShowCover( showCover ); config.setActiveIndicatorRow( activeIndicatorRow ); QDomNodeList rows = elem.elementsByTagName(QStringLiteral("row")); int index = 0; while ( index < rows.size() ) { QDomNode rowNode = rows.item( index ); index++; LayoutItemConfigRow row; QDomNodeList elements = rowNode.toElement().elementsByTagName(QStringLiteral("element")); int index2 = 0; while ( index2 < elements.size() ) { QDomNode elementNode = elements.item( index2 ); index2++; int value = columnForName( elementNode.toElement().attribute( QStringLiteral("value"), QStringLiteral("Title") ) ); QString prefix = elementNode.toElement().attribute( QStringLiteral("prefix"), QString() ); QString sufix = elementNode.toElement().attribute( QStringLiteral("suffix"), QString() ); qreal size = elementNode.toElement().attribute( QStringLiteral("size"), QStringLiteral("0.0") ).toDouble(); bool bold = ( elementNode.toElement().attribute( QStringLiteral("bold"), QStringLiteral("false") ).compare( QLatin1String("true"), Qt::CaseInsensitive ) == 0 ); bool italic = ( elementNode.toElement().attribute( QStringLiteral("italic"), QStringLiteral("false") ).compare( QLatin1String("true"), Qt::CaseInsensitive ) == 0 ); bool underline = ( elementNode.toElement().attribute( QStringLiteral("underline"), QStringLiteral("false") ).compare( QLatin1String("true"), Qt::CaseInsensitive ) == 0 ); QString alignmentString = elementNode.toElement().attribute( QStringLiteral("alignment"), QStringLiteral("left") ); Qt::Alignment alignment; if ( alignmentString.compare( QLatin1String("left"), Qt::CaseInsensitive ) == 0 ) alignment = Qt::AlignLeft | Qt::AlignVCenter; else if ( alignmentString.compare( QLatin1String("right"), Qt::CaseInsensitive ) == 0 ) alignment = Qt::AlignRight| Qt::AlignVCenter; else alignment = Qt::AlignCenter| Qt::AlignVCenter; row.addElement( LayoutItemConfigRowElement( value, size, bold, italic, underline, alignment, prefix, sufix ) ); } config.addRow( row ); } return config; } PlaylistLayout LayoutManager::layout( const QString &layout ) const { return m_layouts.value( layout ); } void LayoutManager::addUserLayout( const QString &name, PlaylistLayout layout ) { layout.setEditable( true ); if( m_layouts.find( name ) != m_layouts.end() ) m_layouts.remove( name ); else m_layoutNames.append( name ); m_layouts.insert( name, layout ); QDomDocument doc( QStringLiteral("layouts") ); QDomElement layouts_element = doc.createElement( QStringLiteral("playlist_layouts") ); QDomElement newLayout = doc.createElement( ("layout" ) ); newLayout.setAttribute( QStringLiteral("name"), name ); doc.appendChild( layouts_element ); layouts_element.appendChild( newLayout ); Q_EMIT( layoutListChanged() ); QDomElement body = doc.createElement( QStringLiteral("body") ); QDomElement single = doc.createElement( QStringLiteral("single") ); newLayout.appendChild( createItemElement( doc, QStringLiteral("single_track"), layout.layoutForPart( PlaylistLayout::Single ) ) ); newLayout.appendChild( createItemElement( doc, QStringLiteral("group_head"), layout.layoutForPart( PlaylistLayout::Head ) ) ); newLayout.appendChild( createItemElement( doc, QStringLiteral("group_body"), layout.layoutForPart( PlaylistLayout::StandardBody ) ) ); newLayout.appendChild( createItemElement( doc, QStringLiteral("group_variousArtistsBody"), layout.layoutForPart( PlaylistLayout::VariousArtistsBody ) ) ); if( layout.inlineControls() ) newLayout.setAttribute( QStringLiteral("inline_controls"), QStringLiteral("true") ); if( layout.tooltips() ) newLayout.setAttribute( QStringLiteral("tooltips"), QStringLiteral("true") ); newLayout.setAttribute( QStringLiteral("group_by"), layout.groupBy() ); QDir layoutsDir = QDir( Amarok::saveLocation( QStringLiteral("playlist_layouts/") ) ); //make sure that this directory exists if ( !layoutsDir.exists() ) layoutsDir.mkpath( Amarok::saveLocation( QStringLiteral("playlist_layouts/") ) ); QFile file( layoutsDir.filePath( name + ".xml" ) ); if ( !file.open(QIODevice::WriteOnly | QIODevice::Text) ) return; QTextStream out( &file ); out << doc.toString(); } QDomElement LayoutManager::createItemElement( QDomDocument doc, const QString &name, const LayoutItemConfig & item ) const { QDomElement element = doc.createElement( name ); QString showCover = item.showCover() ? "true" : "false"; element.setAttribute ( QStringLiteral("show_cover"), showCover ); element.setAttribute ( QStringLiteral("active_indicator_row"), QString::number( item.activeIndicatorRow() ) ); for( int i = 0; i < item.rows(); i++ ) { LayoutItemConfigRow row = item.row( i ); QDomElement rowElement = doc.createElement( QStringLiteral("row") ); element.appendChild( rowElement ); for( int j = 0; j < row.count(); j++ ) { LayoutItemConfigRowElement element = row.element( j ); QDomElement elementElement = doc.createElement( QStringLiteral("element") ); elementElement.setAttribute ( QStringLiteral("prefix"), element.prefix() ); elementElement.setAttribute ( QStringLiteral("suffix"), element.suffix() ); elementElement.setAttribute ( QStringLiteral("value"), internalColumnName( static_cast( element.value() ) ) ); elementElement.setAttribute ( QStringLiteral("size"), QString::number( element.size() ) ); elementElement.setAttribute ( QStringLiteral("bold"), element.bold() ? "true" : "false" ); elementElement.setAttribute ( QStringLiteral("italic"), element.italic() ? "true" : "false" ); elementElement.setAttribute ( QStringLiteral("underline"), element.underline() ? "true" : "false" ); QString alignmentString; if ( element.alignment() & Qt::AlignLeft ) alignmentString = QStringLiteral("left"); else if ( element.alignment() & Qt::AlignRight ) alignmentString = QStringLiteral("right"); else alignmentString = QStringLiteral("center"); elementElement.setAttribute ( QStringLiteral("alignment"), alignmentString ); rowElement.appendChild( elementElement ); } } return element; } bool LayoutManager::isDefaultLayout( const QString & layout ) const { if ( m_layouts.keys().contains( layout ) ) return !m_layouts.value( layout ).isEditable(); return false; } QString LayoutManager::activeLayoutName() const { return m_activeLayout; } void LayoutManager::deleteLayout( const QString &layout ) { //check if layout is editable if ( m_layouts.value( layout ).isEditable() ) { QDir layoutsDir = QDir( Amarok::saveLocation( QStringLiteral("playlist_layouts/") ) ); QString xmlFile = layoutsDir.path() + QLatin1Char('/') + layout + ".xml"; if ( !QFile::remove( xmlFile ) ) debug() << "error deleting file" << xmlFile; m_layouts.remove( layout ); m_layoutNames.removeAll( layout ); Q_EMIT( layoutListChanged() ); if ( layout == m_activeLayout ) setActiveLayout( QStringLiteral("Default") ); } else KMessageBox::sorry( nullptr, i18n( "The layout '%1' is one of the default layouts and cannot be deleted.", layout ), i18n( "Cannot Delete Default Layouts" ) ); } bool LayoutManager::isDeleteable( const QString &layout ) const { return m_layouts.value( layout ).isEditable(); } int LayoutManager::moveUp( const QString &layout ) { int index = m_layoutNames.indexOf( layout ); if ( index > 0 ) { m_layoutNames.swap ( index, index - 1 ); Q_EMIT( layoutListChanged() ); storeLayoutOrdering(); return index - 1; } return index; } int LayoutManager::moveDown( const QString &layout ) { int index = m_layoutNames.indexOf( layout ); if ( index < m_layoutNames.size() -1 ) { m_layoutNames.swap ( index, index + 1 ); Q_EMIT( layoutListChanged() ); storeLayoutOrdering(); return index + 1; } return index; } void LayoutManager::orderLayouts() { KConfigGroup config = Amarok::config( QStringLiteral("Playlist Layout") ); QString orderString = config.readEntry( "Order", "Default" ); QStringList knownLayouts = m_layouts.keys(); QStringList orderingList = orderString.split( QLatin1Char(';'), QString::SkipEmptyParts ); foreach( const QString &layout, orderingList ) { if ( knownLayouts.contains( layout ) ) { //skip any layout names that are in config but that we don't know. Perhaps someone manually deleted a layout file m_layoutNames.append( layout ); knownLayouts.removeAll( layout ); } } //now add any layouts that were not in the order config to end of list: foreach( const QString &layout, knownLayouts ) m_layoutNames.append( layout ); } } //namespace Playlist void Playlist::LayoutManager::storeLayoutOrdering() { QString ordering; foreach( const QString &name, m_layoutNames ) { ordering += name; ordering += ';'; } if ( !ordering.isEmpty() ) ordering.chop( 1 ); //remove trailing; KConfigGroup config = Amarok::config(QStringLiteral("Playlist Layout")); config.writeEntry( "Order", ordering ); } diff --git a/src/playlist/layouts/PlaylistLayoutEditDialog.cpp b/src/playlist/layouts/PlaylistLayoutEditDialog.cpp index 17d051230c..08753ae304 100644 --- a/src/playlist/layouts/PlaylistLayoutEditDialog.cpp +++ b/src/playlist/layouts/PlaylistLayoutEditDialog.cpp @@ -1,531 +1,531 @@ /**************************************************************************************** * Copyright (c) 2009 Nikolaj Hald Nielsen * * Copyright (c) 2009 Téo Mrnjavac * * Copyright (c) 2010 Oleksandr Khayrullin * * * * 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 "PlaylistLayoutEditDialog.h" #include "core/support/Debug.h" #include "playlist/layouts/LayoutManager.h" #include "playlist/PlaylistDefines.h" #include #include #include Playlist::PlaylistLayoutEditDialog::PlaylistLayoutEditDialog( QWidget *parent ) : QDialog( parent ) { setupUi( this ); // -- add tokens to the token pool Column tokenValues[] = { Album, AlbumArtist, Artist, Bitrate, Bpm, Comment, Composer, Directory, DiscNumber, Divider, Filename, Filesize, Genre, GroupLength, GroupTracks, LastPlayed, Labels, Length, Moodbar, PlaceHolder, PlayCount, Rating, SampleRate, Score, Source, Title, TitleWithTrackNum, TrackNumber, Type, Year }; for( uint i = 0; i < sizeof( tokenValues ) / sizeof( tokenValues[0] ); i++ ) tokenPool->addToken( new Token( columnName( tokenValues[i] ), iconName( tokenValues[i] ), static_cast(tokenValues[i]) ) ); m_firstActiveLayout = LayoutManager::instance()->activeLayoutName(); //add an editor to each tab for( int part = 0; part < PlaylistLayout::NumParts; part++ ) m_partsEdit[part] = new Playlist::LayoutEditWidget( this ); m_layoutsMap = new QMap(); elementTabs->addTab( m_partsEdit[PlaylistLayout::Head], i18n( "Head" ) ); elementTabs->addTab( m_partsEdit[PlaylistLayout::StandardBody], i18n( "Body" ) ); elementTabs->addTab( m_partsEdit[PlaylistLayout::VariousArtistsBody], i18n( "Body (Various artists)" ) ); elementTabs->addTab( m_partsEdit[PlaylistLayout::Single], i18n( "Single" ) ); QStringList layoutNames = LayoutManager::instance()->layouts(); foreach( const QString &layoutName, layoutNames ) { PlaylistLayout layout = LayoutManager::instance()->layout( layoutName ); layout.setDirty( false ); m_layoutsMap->insert( layoutName, layout ); } layoutListWidget->addItems( layoutNames ); layoutListWidget->setCurrentRow( LayoutManager::instance()->layouts().indexOf( LayoutManager::instance()->activeLayoutName() ) ); setupGroupByCombo(); if ( layoutListWidget->currentItem() ) setLayout( layoutListWidget->currentItem()->text() ); connect( previewButton, &QAbstractButton::clicked, this, &PlaylistLayoutEditDialog::preview ); connect( layoutListWidget, &QListWidget::currentTextChanged, this, &PlaylistLayoutEditDialog::setLayout ); connect( layoutListWidget, &QListWidget::currentRowChanged, this, &PlaylistLayoutEditDialog::toggleEditButtons ); connect( layoutListWidget, &QListWidget::currentRowChanged, this, &PlaylistLayoutEditDialog::toggleUpDownButtons ); connect( moveUpButton, &QAbstractButton::clicked, this, &PlaylistLayoutEditDialog::moveUp ); connect( moveDownButton, &QAbstractButton::clicked, this, &PlaylistLayoutEditDialog::moveDown ); buttonBox->button(QDialogButtonBox::Apply)->setIcon( QIcon::fromTheme( QStringLiteral("dialog-ok-apply") ) ); buttonBox->button(QDialogButtonBox::Ok)->setIcon( QIcon::fromTheme( QStringLiteral("dialog-ok") ) ); buttonBox->button(QDialogButtonBox::Cancel)->setIcon( QIcon::fromTheme( QStringLiteral("dialog-cancel") ) ); connect( buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &PlaylistLayoutEditDialog::apply ); - const QIcon newIcon( "document-new" ); + const QIcon newIcon = QIcon::fromTheme( QStringLiteral("document-new") ); newLayoutButton->setIcon( newIcon ); newLayoutButton->setToolTip( i18n( "New playlist layout" ) ); connect( newLayoutButton, &QAbstractButton::clicked, this, &PlaylistLayoutEditDialog::newLayout ); - const QIcon copyIcon( "edit-copy" ); + const QIcon copyIcon = QIcon::fromTheme( QStringLiteral("edit-copy") ); copyLayoutButton->setIcon( copyIcon ); copyLayoutButton->setToolTip( i18n( "Copy playlist layout" ) ); connect( copyLayoutButton, &QAbstractButton::clicked, this, &PlaylistLayoutEditDialog::copyLayout ); - const QIcon deleteIcon( "edit-delete" ); + const QIcon deleteIcon( QStringLiteral("edit-delete") ); deleteLayoutButton->setIcon( deleteIcon ); deleteLayoutButton->setToolTip( i18n( "Delete playlist layout" ) ); connect( deleteLayoutButton, &QAbstractButton::clicked, this, &PlaylistLayoutEditDialog::deleteLayout ); - const QIcon renameIcon( "edit-rename" ); + const QIcon renameIcon = QIcon::fromTheme( QStringLiteral("edit-rename") ); renameLayoutButton->setIcon( renameIcon ); renameLayoutButton->setToolTip( i18n( "Rename playlist layout" ) ); connect( renameLayoutButton, &QAbstractButton::clicked, this, &PlaylistLayoutEditDialog::renameLayout ); toggleEditButtons(); toggleUpDownButtons(); for( int part = 0; part < PlaylistLayout::NumParts; part++ ) connect( m_partsEdit[part], &Playlist::LayoutEditWidget::changed, this, &PlaylistLayoutEditDialog::setLayoutChanged ); connect( inlineControlsChekbox, &QCheckBox::stateChanged, this, &PlaylistLayoutEditDialog::setLayoutChanged ); connect( tooltipsCheckbox, &QCheckBox::stateChanged, this, &PlaylistLayoutEditDialog::setLayoutChanged ); connect( groupByComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &PlaylistLayoutEditDialog::setLayoutChanged ); } Playlist::PlaylistLayoutEditDialog::~PlaylistLayoutEditDialog() { } void Playlist::PlaylistLayoutEditDialog::newLayout() //SLOT { bool ok; QString layoutName = QInputDialog::getText( this, i18n( "Choose a name for the new playlist layout" ), i18n( "Please enter a name for the playlist layout you are about to define:" ), QLineEdit::Normal, QString(), &ok ); if( !ok ) return; if( layoutName.isEmpty() ) { KMessageBox::sorry( this, i18n( "Cannot create a layout with no name." ), i18n( "Layout name error" ) ); return; } if( m_layoutsMap->keys().contains( layoutName ) ) { KMessageBox::sorry( this, i18n( "Cannot create a layout with the same name as an existing layout." ), i18n( "Layout name error" ) ); return; } if( layoutName.contains( QLatin1Char('/') ) ) { KMessageBox::sorry( this, i18n( "Cannot create a layout containing '/'." ), i18n( "Layout name error" ) ); return; } PlaylistLayout layout; layout.setEditable( true ); //Should I use true, TRUE or 1? layout.setDirty( true ); layoutListWidget->addItem( layoutName ); layoutListWidget->setCurrentItem( (layoutListWidget->findItems( layoutName, Qt::MatchExactly ) ).first() ); for( int part = 0; part < PlaylistLayout::NumParts; part++ ) { m_partsEdit[part]->clear(); layout.setLayoutForPart( (PlaylistLayout::Part)part, m_partsEdit[part]->config() ); } m_layoutsMap->insert( layoutName, layout ); LayoutManager::instance()->addUserLayout( layoutName, layout ); setLayout( layoutName ); } void Playlist::PlaylistLayoutEditDialog::copyLayout() { LayoutItemConfig configs[PlaylistLayout::NumParts]; for( int part = 0; part < PlaylistLayout::NumParts; part++ ) configs[part] = m_partsEdit[part]->config(); QString layoutName = layoutListWidget->currentItem()->text(); bool ok; layoutName = QInputDialog::getText( this, i18n( "Choose a name for the new playlist layout" ), i18n( "Please enter a name for the playlist layout you are about to define as copy of the layout '%1':", layoutName ), QLineEdit::Normal, layoutName, &ok ); if( !ok) return; if( layoutName.isEmpty() ) { KMessageBox::sorry( this, i18n( "Cannot create a layout with no name." ), i18n( "Layout name error" ) ); return; } if( m_layoutsMap->keys().contains( layoutName ) ) { KMessageBox::sorry( this, i18n( "Cannot create a layout with the same name as an existing layout." ), i18n( "Layout name error" ) ); return; } //layoutListWidget->addItem( layoutName ); PlaylistLayout layout; layout.setEditable( true ); //Should I use true, TRUE or 1? layout.setDirty( true ); configs[PlaylistLayout::Head].setActiveIndicatorRow( -1 ); for( int part = 0; part < PlaylistLayout::NumParts; part++ ) layout.setLayoutForPart( (PlaylistLayout::Part)part, configs[part] ); layout.setInlineControls( inlineControlsChekbox->isChecked() ); layout.setTooltips( tooltipsCheckbox->isChecked() ); layout.setGroupBy( groupByComboBox->itemData( groupByComboBox->currentIndex() ).toString() ); LayoutManager::instance()->addUserLayout( layoutName, layout ); //reload from manager: layoutListWidget->clear(); layoutListWidget->addItems( LayoutManager::instance()->layouts() ); m_layoutsMap->insert( layoutName, layout ); layoutListWidget->setCurrentItem( ( layoutListWidget->findItems( layoutName, Qt::MatchExactly ) ).first() ); setLayout( layoutName ); } void Playlist::PlaylistLayoutEditDialog::deleteLayout() //SLOT { m_layoutsMap->remove( layoutListWidget->currentItem()->text() ); if( LayoutManager::instance()->layouts().contains( layoutListWidget->currentItem()->text() ) ) //if the layout is already saved in the LayoutManager LayoutManager::instance()->deleteLayout( layoutListWidget->currentItem()->text() ); //delete it delete layoutListWidget->currentItem(); } void Playlist::PlaylistLayoutEditDialog::renameLayout() { PlaylistLayout layout = m_layoutsMap->value( layoutListWidget->currentItem()->text() ); QString layoutName; while( layoutName.isEmpty() || m_layoutsMap->keys().contains( layoutName ) ) { bool ok; layoutName = QInputDialog::getText( this, i18n( "Choose a new name for the playlist layout" ), i18n( "Please enter a new name for the playlist layout you are about to rename:" ), QLineEdit::Normal, layoutListWidget->currentItem()->text(), &ok ); if ( !ok ) { //Cancelled so just return return; } if( layoutName.isEmpty() ) KMessageBox::sorry( this, i18n( "Cannot rename a layout to have no name." ), i18n( "Layout name error" ) ); if( m_layoutsMap->keys().contains( layoutName ) ) KMessageBox::sorry( this, i18n( "Cannot rename a layout to have the same name as an existing layout." ), i18n( "Layout name error" ) ); } m_layoutsMap->remove( layoutListWidget->currentItem()->text() ); if( LayoutManager::instance()->layouts().contains( layoutListWidget->currentItem()->text() ) ) //if the layout is already saved in the LayoutManager LayoutManager::instance()->deleteLayout( layoutListWidget->currentItem()->text() ); //delete it LayoutManager::instance()->addUserLayout( layoutName, layout ); m_layoutsMap->insert( layoutName, layout ); layoutListWidget->currentItem()->setText( layoutName ); setLayout( layoutName ); } void Playlist::PlaylistLayoutEditDialog::setLayout( const QString &layoutName ) //SLOT { DEBUG_BLOCK m_layoutName = layoutName; if( m_layoutsMap->keys().contains( layoutName ) ) //is the layout exists in the list of loaded layouts { debug() << "loaded layout"; PlaylistLayout layout = m_layoutsMap->value( layoutName ); for( int part = 0; part < PlaylistLayout::NumParts; part++ ) m_partsEdit[part]->readLayout( layout.layoutForPart( (PlaylistLayout::Part)part ) ); inlineControlsChekbox->setChecked( layout.inlineControls() ); tooltipsCheckbox->setChecked( layout.tooltips() ); groupByComboBox->setCurrentIndex( groupByComboBox->findData( layout.groupBy() ) ); setEnabledTabs(); //make sure that it is not marked dirty (it will be because of the changed signal triggering when loading it) //unless it is actually changed debug() << "not dirty anyway!!"; (*m_layoutsMap)[m_layoutName].setDirty( false ); } else { for( int part = 0; part < PlaylistLayout::NumParts; part++ ) m_partsEdit[part]->clear(); } } void Playlist::PlaylistLayoutEditDialog::preview() { PlaylistLayout layout; for( int part = 0; part < PlaylistLayout::NumParts; part++ ) { LayoutItemConfig config = m_partsEdit[part]->config(); if( part == PlaylistLayout::Head ) config.setActiveIndicatorRow( -1 ); layout.setLayoutForPart( (PlaylistLayout::Part)part, config ); } layout.setInlineControls( inlineControlsChekbox->isChecked() ); layout.setTooltips( tooltipsCheckbox->isChecked() ); layout.setGroupBy( groupByComboBox->itemData( groupByComboBox->currentIndex() ).toString() ); LayoutManager::instance()->setPreviewLayout( layout ); } void Playlist::PlaylistLayoutEditDialog::toggleEditButtons() //SLOT { if ( !layoutListWidget->currentItem() ) { deleteLayoutButton->setEnabled( 0 ); renameLayoutButton->setEnabled( 0 ); } else if( LayoutManager::instance()->isDefaultLayout( layoutListWidget->currentItem()->text() ) ) { deleteLayoutButton->setEnabled( 0 ); renameLayoutButton->setEnabled( 0 ); } else { deleteLayoutButton->setEnabled( 1 ); renameLayoutButton->setEnabled( 1 ); } } void Playlist::PlaylistLayoutEditDialog::toggleUpDownButtons() { if ( !layoutListWidget->currentItem() ) { moveUpButton->setEnabled( 0 ); moveDownButton->setEnabled( 0 ); } else if ( layoutListWidget->currentRow() == 0 ) { moveUpButton->setEnabled( 0 ); if ( layoutListWidget->currentRow() >= m_layoutsMap->size() -1 ) moveDownButton->setEnabled( 0 ); else moveDownButton->setEnabled( 1 ); } else if ( layoutListWidget->currentRow() >= m_layoutsMap->size() -1 ) { moveDownButton->setEnabled( 0 ); moveUpButton->setEnabled( 1 ); //we already checked that this is not row 0 } else { moveDownButton->setEnabled( 1 ); moveUpButton->setEnabled( 1 ); } } void Playlist::PlaylistLayoutEditDialog::apply() //SLOT { foreach( const QString &layoutName, m_layoutsMap->keys() ) { PlaylistLayout layout = m_layoutsMap->value( layoutName ); if( layout.isDirty() ) { // search a new name for changed default layouts if( LayoutManager::instance()->isDefaultLayout( layoutName ) ) { QString newLayoutName = i18n( "copy of %1", layoutName ); QString orgCopyName = newLayoutName; int copyNumber = 1; QStringList existingLayouts = LayoutManager::instance()->layouts(); while( existingLayouts.contains( newLayoutName ) ) { copyNumber++; newLayoutName = i18nc( "adds a copy number to a generated name if the name already exists, for instance 'copy of Foo 2' if 'copy of Foo' is taken", "%1 %2", orgCopyName, copyNumber ); } const QString msg = i18n( "The layout '%1' you modified is one of the default layouts and cannot be overwritten. " "Saved as new layout '%2'", layoutName, newLayoutName ); KMessageBox::sorry( this, msg, i18n( "Default Layout" ) ); layout.setDirty( false ); m_layoutsMap->insert( newLayoutName, layout ); LayoutManager::instance()->addUserLayout( newLayoutName, layout ); layoutListWidget->addItem( newLayoutName ); if( layoutName == m_layoutName ) layoutListWidget->setCurrentItem( ( layoutListWidget->findItems( newLayoutName, Qt::MatchExactly ) ).first() ); // restore the default layout m_layoutsMap->insert( layoutName, LayoutManager::instance()->layout( layoutName ) ); } else { layout.setDirty( false ); m_layoutsMap->insert( layoutName, layout ); LayoutManager::instance()->addUserLayout( layoutName, layout ); } } } LayoutManager::instance()->setActiveLayout( layoutListWidget->currentItem()->text() ); //important to override the previewed layout if preview is used } void Playlist::PlaylistLayoutEditDialog::accept() //SLOT { apply(); QDialog::accept(); } void Playlist::PlaylistLayoutEditDialog::reject() //SLOT { DEBUG_BLOCK debug() << "Applying initial layout: " << m_firstActiveLayout; if( layoutListWidget->findItems( m_firstActiveLayout, Qt::MatchExactly ).isEmpty() ) LayoutManager::instance()->setActiveLayout( QStringLiteral("Default") ); else LayoutManager::instance()->setActiveLayout( m_firstActiveLayout ); QDialog::reject(); } void Playlist::PlaylistLayoutEditDialog::moveUp() { int newRow = LayoutManager::instance()->moveUp( m_layoutName ); layoutListWidget->clear(); layoutListWidget->addItems( LayoutManager::instance()->layouts() ); layoutListWidget->setCurrentRow( newRow ); } void Playlist::PlaylistLayoutEditDialog::moveDown() { int newRow = LayoutManager::instance()->moveDown( m_layoutName ); layoutListWidget->clear(); layoutListWidget->addItems( LayoutManager::instance()->layouts() ); layoutListWidget->setCurrentRow( newRow ); } void Playlist::PlaylistLayoutEditDialog::setEnabledTabs() { DEBUG_BLOCK //Enable or disable tabs depending on whether grouping is allowed. QString grouping = groupByComboBox->itemData( groupByComboBox->currentIndex() ).toString(); bool groupingEnabled = ( !grouping.isEmpty() && grouping != QLatin1String("None") ); if ( !groupingEnabled ) elementTabs->setCurrentWidget( m_partsEdit[PlaylistLayout::Single] ); debug() << groupByComboBox->itemData( groupByComboBox->currentIndex() ).toString(); debug() << groupingEnabled; elementTabs->setTabEnabled( elementTabs->indexOf( m_partsEdit[PlaylistLayout::Head] ), groupingEnabled ); elementTabs->setTabEnabled( elementTabs->indexOf( m_partsEdit[PlaylistLayout::StandardBody] ), groupingEnabled ); elementTabs->setTabEnabled( elementTabs->indexOf( m_partsEdit[PlaylistLayout::VariousArtistsBody] ), groupingEnabled ); } //Sets up a combo box that presents the possible grouping categories, as well as the option //to perform no grouping. //We'll use the "user data" to store the un-i18n-ized category name for internal use. void Playlist::PlaylistLayoutEditDialog::setupGroupByCombo() { foreach( const Playlist::Column &col, Playlist::groupableCategories() ) { groupByComboBox->addItem( QIcon::fromTheme( iconName( col ) ), columnName( col ), QVariant( internalColumnName( col ) ) ); } //Add the option to not perform grouping //Use a null string to specify "no grouping" - groupByComboBox->addItem( i18n( "No Grouping" ), QVariant( "None" ) ); + groupByComboBox->addItem( i18n( "No Grouping" ), QVariant( QStringLiteral("None") ) ); } void Playlist::PlaylistLayoutEditDialog::setLayoutChanged() { DEBUG_BLOCK setEnabledTabs(); for( int part = 0; part < PlaylistLayout::NumParts; part++ ) (*m_layoutsMap)[m_layoutName].setLayoutForPart( (PlaylistLayout::Part)part, m_partsEdit[part]->config() ); (*m_layoutsMap)[m_layoutName].setInlineControls( inlineControlsChekbox->isChecked() ); (*m_layoutsMap)[m_layoutName].setTooltips( tooltipsCheckbox->isChecked() ); (*m_layoutsMap)[m_layoutName].setGroupBy( groupByComboBox->itemData( groupByComboBox->currentIndex() ).toString() ); (*m_layoutsMap)[m_layoutName].setDirty( true ); } diff --git a/src/playlist/proxymodels/ProxyBase.cpp b/src/playlist/proxymodels/ProxyBase.cpp index b22fcad2ec..64c374c236 100644 --- a/src/playlist/proxymodels/ProxyBase.cpp +++ b/src/playlist/proxymodels/ProxyBase.cpp @@ -1,386 +1,386 @@ /**************************************************************************************** * Copyright (c) 2009 Téo Mrnjavac * * Copyright (c) 2010 Nanno Langstraat * * * * 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 "ProxyBase.h" #include "core/meta/Meta.h" #include "core/meta/Statistics.h" #include "core-impl/playlists/types/file/PlaylistFileSupport.h" #include "playlist/PlaylistModel.h" namespace Playlist { ProxyBase::ProxyBase( AbstractModel *belowModel, QObject *parent ) : QSortFilterProxyModel( parent ) , m_belowModel( belowModel ) { setSourceModel( m_belowModel->qaim() ); // Proxy the Playlist::AbstractModel signals. // If you need to do something special in a subclass, disconnect() this signal and // do your own connect() call. connect( qobject_cast( sourceModel() ), &Playlist::Model::activeTrackChanged, this, &ProxyBase::activeTrackChanged ); connect( qobject_cast( sourceModel() ), &Playlist::Model::queueChanged, this, &ProxyBase::queueChanged ); } ProxyBase::~ProxyBase() {} // Pass-through virtual public methods, that pretty much just forward stuff through the stack // of proxies, start here. // Please keep them sorted alphabetically. -- Téo quint64 ProxyBase::activeId() const { return m_belowModel->activeId(); } int ProxyBase::activeRow() const { // We map the active row form the source to this ProxyModel. return rowFromSource( m_belowModel->activeRow() ); } Meta::TrackPtr ProxyBase::activeTrack() const { return m_belowModel->activeTrack(); } QSet ProxyBase::allRowsForTrack( const Meta::TrackPtr& track ) const { QSet proxyModelRows; foreach( int sourceModelRow, m_belowModel->allRowsForTrack( track ) ) { int proxyModelRow = rowFromSource( sourceModelRow ); if ( proxyModelRow != -1 ) proxyModelRows.insert( proxyModelRow ); } return proxyModelRows; } void ProxyBase::clearSearchTerm() { m_belowModel->clearSearchTerm(); } bool ProxyBase::containsTrack( const Meta::TrackPtr& track ) const { return ( firstRowForTrack( track ) != -1 ); // Let him do the clever work. } int ProxyBase::currentSearchFields() { return m_belowModel->currentSearchFields(); } QString ProxyBase::currentSearchTerm() { return m_belowModel->currentSearchTerm(); } bool ProxyBase::exportPlaylist( const QString &path, bool relative ) { return Playlists::exportPlaylistFile( tracks(), QUrl::fromLocalFile(path), relative ); } void ProxyBase::filterUpdated() { m_belowModel->filterUpdated(); } int ProxyBase::find( const QString &searchTerm, int searchFields ) { ProxyBase *proxyBase = dynamic_cast< ProxyBase * >( m_belowModel ); if ( !proxyBase ) return -1; return rowFromSource( proxyBase->find( searchTerm, searchFields ) ); } int ProxyBase::findNext( const QString &searchTerm, int selectedRow, int searchFields ) { ProxyBase *proxyBase = dynamic_cast< ProxyBase * >( m_belowModel ); if ( !proxyBase ) return -1; return rowFromSource( proxyBase->findNext( searchTerm, rowToSource( selectedRow ), searchFields ) ); } int ProxyBase::findPrevious( const QString &searchTerm, int selectedRow, int searchFields ) { ProxyBase *proxyBase = dynamic_cast< ProxyBase * >( m_belowModel ); if ( !proxyBase ) return -1; return rowFromSource( proxyBase->findPrevious( searchTerm, rowToSource( selectedRow ), searchFields ) ); } int ProxyBase::firstRowForTrack( const Meta::TrackPtr& track ) const { // First optimistically try 'firstRowForTrack()'. It'll usually work. int proxyModelRow = rowFromSource( m_belowModel->firstRowForTrack( track ) ); if ( proxyModelRow != -1 ) return proxyModelRow; else { // It might be that there are multiple hits in the source model, and we just got // unlucky with a source row that's filtered out in this model. So, we need to // check all hits. foreach( int sourceModelRow, m_belowModel->allRowsForTrack( track ) ) { proxyModelRow = rowFromSource( sourceModelRow ); if ( proxyModelRow != -1 ) return proxyModelRow; } return -1; } } quint64 ProxyBase::idAt( const int row ) const { if( rowExists( row ) ) return m_belowModel->idAt( rowToSource( row ) ); return 0; } bool ProxyBase::rowExists( int row ) const { QModelIndex index = this->index( row, 0 ); return index.isValid(); } int ProxyBase::rowForId( const quint64 id ) const { return rowFromSource( m_belowModel->rowForId( id ) ); } int ProxyBase::rowFromBottomModel( const int row ) { return rowFromSource( m_belowModel->rowFromBottomModel( row ) ); } int ProxyBase::rowToBottomModel( const int row ) { return m_belowModel->rowToBottomModel( rowToSource( row ) ); } void ProxyBase::setActiveId( const quint64 id ) { m_belowModel->setActiveId( id ); } void ProxyBase::setActiveRow( int row ) { m_belowModel->setActiveRow( rowToSource( row ) ); } void ProxyBase::setAllUnplayed() { m_belowModel->setAllUnplayed(); } void ProxyBase::emitQueueChanged() { Q_ASSERT_X(false, "emitQueueChanged", "queueChanged() should be emitted at the bottom of " "the model stack so it can be received from every model."); } int ProxyBase::queuePositionOfRow( int row ) { return m_belowModel->queuePositionOfRow( rowToSource ( row ) ); } void ProxyBase::showOnlyMatches( bool onlyMatches ) { ProxyBase *proxyBase = dynamic_cast< ProxyBase * >( m_belowModel ); if ( !proxyBase ) return ; proxyBase->showOnlyMatches( onlyMatches ); } Item::State ProxyBase::stateOfId( quint64 id ) const { return m_belowModel->stateOfId( id ); } Item::State ProxyBase::stateOfRow( int row ) const { return m_belowModel->stateOfRow( rowToSource( row ) ); } qint64 ProxyBase::totalLength() const { return m_belowModel->totalLength(); } quint64 ProxyBase::totalSize() const { return m_belowModel->totalSize(); } Meta::TrackPtr ProxyBase::trackAt(int row) const { return m_belowModel->trackAt( rowToSource( row ) ); } Meta::TrackPtr ProxyBase::trackForId( const quint64 id ) const { return m_belowModel->trackForId( id ); } Meta::TrackList ProxyBase::tracks() { Meta::TrackList tl; for( int i = 0; i < rowCount(); ++i ) tl << trackAt( i ); return tl; } //protected: bool ProxyBase::rowMatch( int sourceModelRow, const QString &searchTerms, int searchFields ) const { if ( !m_belowModel ) return false; Meta::TrackPtr track = m_belowModel->trackAt( sourceModelRow ); - QStringList searchList = searchTerms.split(' ', QString::SkipEmptyParts); + QStringList searchList = searchTerms.split(QLatin1Char(' '), QString::SkipEmptyParts); foreach( const QString& searchTerm, searchList ) { bool match = false; if ( searchFields & MatchTrack && track->prettyName().contains( searchTerm, Qt::CaseInsensitive ) ) match = true; if ( searchFields & MatchArtist && track->artist() && track->artist()->prettyName().contains( searchTerm, Qt::CaseInsensitive ) ) match = true; if ( searchFields & MatchAlbum && track->album() && track->album()->prettyName().contains( searchTerm, Qt::CaseInsensitive ) ) match = true; if ( searchFields & MatchGenre && track->genre() && track->genre()->prettyName().contains( searchTerm, Qt::CaseInsensitive ) ) match = true; if ( searchFields & MatchComposer && track->composer() && track->composer()->prettyName().contains( searchTerm, Qt::CaseInsensitive ) ) match = true; if ( searchFields & MatchYear && track->year() && track->year()->prettyName().contains( searchTerm, Qt::CaseInsensitive ) ) match = true; if( searchFields & MatchRating ) { bool ok; int rating = QString( searchTerm ).remove( QStringLiteral("rating:") ).toInt( &ok ); if( ok && ( track->statistics()->rating() == rating ) ) match = true; } if( !match ) return false; } return true; } int ProxyBase::rowFromSource( int sourceModelRow ) const { QModelIndex sourceModelIndex = sourceModel()->index( sourceModelRow, 0 ); QModelIndex proxyModelIndex = mapFromSource( sourceModelIndex ); // Call 'map' even for a 1:1 passthrough proxy: QSFPM might need it. if ( proxyModelIndex.isValid() ) return proxyModelIndex.row(); else return -1; } int ProxyBase::rowToSource( int proxyModelRow ) const { QModelIndex proxyModelIndex = this->index( proxyModelRow, 0 ); QModelIndex sourceModelIndex = mapToSource( proxyModelIndex ); // Call 'map' even for a 1:1 passthrough proxy: QSFPM might need it. if( sourceModelIndex.isValid() ) return sourceModelIndex.row(); else if( proxyModelRow == rowCount() ) return sourceModel()->rowCount(); else return -1; } } //namespace Playlist diff --git a/src/playlistmanager/file/PlaylistFileProvider.cpp b/src/playlistmanager/file/PlaylistFileProvider.cpp index d11fb726ae..053c09c59f 100644 --- a/src/playlistmanager/file/PlaylistFileProvider.cpp +++ b/src/playlistmanager/file/PlaylistFileProvider.cpp @@ -1,346 +1,346 @@ /**************************************************************************************** * Copyright (c) 2007 Bart Cerneels * * * * 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 "PlaylistFileProvider.h" #include "App.h" #include "core-impl/playlists/types/file/PlaylistFileSupport.h" #include "core/support/Amarok.h" #include "core/support/Debug.h" #include "core/support/Components.h" #include "core/logger/Logger.h" #include "core-impl/playlists/types/file/asx/ASXPlaylist.h" #include "core-impl/playlists/types/file/m3u/M3UPlaylist.h" #include "core-impl/playlists/types/file/pls/PLSPlaylist.h" #include "core-impl/playlists/types/file/xspf/XSPFPlaylist.h" #include "playlist/PlaylistModelStack.h" #include "playlistmanager/PlaylistManager.h" #include #include #include #include #include #include #include #include #include using Playlist::ModelStack; namespace Playlists { PlaylistFileProvider::PlaylistFileProvider() : UserPlaylistProvider() , m_playlistsLoaded( false ) , m_saveLaterTimer( 0 ) { //playlists are lazy loaded but we can count how many we'll load already QStringList keys = loadedPlaylistsConfig().keyList(); foreach( const QString &key, keys ) { QUrl url( key ); //Don't load these from the config file, they are read from the directory anyway if( KIO::upUrl(url).matches( QUrl::fromUserInput(Amarok::saveLocation(QStringLiteral("playlists"))), QUrl::StripTrailingSlash ) ) continue; m_urlsToLoad << url; } //also add all files in the $KDEHOME/share/apps/amarok/playlists QDir playlistDir = QDir( Amarok::saveLocation( QStringLiteral("playlists") ), QLatin1String(""), QDir::Name, QDir::Files | QDir::Readable ); foreach( const QString &file, playlistDir.entryList() ) { QUrl url( playlistDir.path() ); url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + QLatin1Char('/') + ( file )); if( Playlists::isPlaylist( url ) ) m_urlsToLoad << url; } } PlaylistFileProvider::~PlaylistFileProvider() { DEBUG_BLOCK //remove all, well add them again soon loadedPlaylistsConfig().deleteGroup(); //Write loaded playlists to config file foreach( Playlists::PlaylistFilePtr playlistFile, m_playlists ) { QUrl url = playlistFile->uidUrl(); //only save files NOT in "playlists", those are automatically loaded. if( KIO::upUrl(url).matches( QUrl::fromUserInput(Amarok::saveLocation( QStringLiteral("playlists") )), QUrl::StripTrailingSlash ) ) continue; //debug() << "storing to rc-file: " << url.url(); loadedPlaylistsConfig().writeEntry( url.url(), playlistFile->groups() ); } loadedPlaylistsConfig().sync(); } QString PlaylistFileProvider::prettyName() const { return i18n( "Playlist Files on Disk" ); } QIcon PlaylistFileProvider::icon() const { return QIcon::fromTheme( "folder-documents" ); } int PlaylistFileProvider::playlistCount() const { return m_playlists.count() + m_urlsToLoad.count(); } Playlists::PlaylistList PlaylistFileProvider::playlists() { Playlists::PlaylistList playlists; if( !m_playlistsLoaded ) { //trigger a lazy load the playlists QTimer::singleShot(0, this, &PlaylistFileProvider::loadPlaylists ); return playlists; } foreach( const Playlists::PlaylistFilePtr &playlistFile, m_playlists ) { Playlists::PlaylistPtr playlist = Playlists::PlaylistPtr::dynamicCast( playlistFile ); if( !playlist.isNull() ) playlists << playlist; } return playlists; } Playlists::PlaylistPtr PlaylistFileProvider::save( const Meta::TrackList &tracks, const QString &name ) { DEBUG_BLOCK QString filename = name.isEmpty() ? QDateTime::currentDateTime().toString( QStringLiteral("ddd MMMM d yy hh-mm")) : name; filename.replace( QLatin1Char('/'), QLatin1Char('-') ); filename.replace( QLatin1Char('\\'), QLatin1Char('-') ); Playlists::PlaylistFormat format = Playlists::getFormat( QUrl::fromUserInput(filename) ); if( format == Playlists::Unknown ) // maybe the name just had a dot in it. We just add .xspf { format = Playlists::XSPF; filename.append( QLatin1String( ".xspf" ) ); } QUrl path( Amarok::saveLocation( QStringLiteral("playlists") ) ); path = path.adjusted(QUrl::StripTrailingSlash); path.setPath(path.path() + QLatin1Char('/') + ( Amarok::vfatPath( filename ) )); if( QFileInfo( path.toLocalFile() ).exists() ) { //TODO:request overwrite return Playlists::PlaylistPtr(); } Playlists::PlaylistFile *playlistFile = 0; switch( format ) { case Playlists::ASX: playlistFile = new Playlists::ASXPlaylist( path, this ); break; case Playlists::PLS: playlistFile = new Playlists::PLSPlaylist( path, this ); break; case Playlists::M3U: playlistFile = new Playlists::M3UPlaylist( path, this ); break; case Playlists::XSPF: playlistFile = new Playlists::XSPFPlaylist( path, this ); break; case Playlists::XML: case Playlists::RAM: case Playlists::SMIL: case Playlists::Unknown: // this should not happen since we set the format to XSPF above. return Playlists::PlaylistPtr(); } playlistFile->setName( filename ); playlistFile->addTracks( tracks ); playlistFile->save( true ); Playlists::PlaylistFilePtr playlistPtr( playlistFile ); m_playlists << playlistPtr; //just in case there wasn't one loaded before. m_playlistsLoaded = true; Playlists::PlaylistPtr playlist( playlistFile ); Q_EMIT playlistAdded( playlist ); return playlist; } bool PlaylistFileProvider::import( const QUrl &path ) { DEBUG_BLOCK if( !path.isValid() ) { error() << "path is not valid!"; return false; } foreach( Playlists::PlaylistFilePtr playlistFile, m_playlists ) { if( !playlistFile ) { error() << "Could not cast down."; error() << "m_playlists got corrupted! " << __FILE__ << ":" << __LINE__; continue; } if( playlistFile->uidUrl() == path ) { debug() << "Playlist " << path.path() << " was already imported"; return false; } } debug() << "Importing playlist file " << path; if( path == QUrl::fromLocalFile(Amarok::defaultPlaylistPath()) ) { error() << "trying to load saved session playlist at %s" << path.path(); return false; } Playlists::PlaylistFilePtr playlistFile = Playlists::loadPlaylistFile( path, this ); if( !playlistFile ) return false; m_playlists << playlistFile; //just in case there wasn't one loaded before. m_playlistsLoaded = true; Q_EMIT playlistAdded( PlaylistPtr( playlistFile.data() ) ); return true; } void PlaylistFileProvider::renamePlaylist(PlaylistPtr playlist, const QString &newName ) { DEBUG_BLOCK playlist->setName( newName ); } bool PlaylistFileProvider::deletePlaylists( const Playlists::PlaylistList &playlists ) { Playlists::PlaylistFileList playlistFiles; foreach( Playlists::PlaylistPtr playlist, playlists ) { Playlists::PlaylistFilePtr playlistFile = Playlists::PlaylistFilePtr::dynamicCast( playlist ); if( !playlistFile.isNull() ) playlistFiles << playlistFile; } return deletePlaylistFiles( playlistFiles ); } bool PlaylistFileProvider::deletePlaylistFiles( Playlists::PlaylistFileList playlistFiles ) { foreach( Playlists::PlaylistFilePtr playlistFile, playlistFiles ) { m_playlists.removeAll( playlistFile ); loadedPlaylistsConfig().deleteEntry( playlistFile->uidUrl().url() ); QFile::remove( playlistFile->uidUrl().path() ); Q_EMIT playlistRemoved( Playlists::PlaylistPtr::dynamicCast( playlistFile ) ); } loadedPlaylistsConfig().sync(); return true; } void PlaylistFileProvider::loadPlaylists() { if( m_urlsToLoad.isEmpty() ) return; //arbitrary number of playlists to load during one mainloop run: 5 for( int i = 0; i < qMin( m_urlsToLoad.count(), 5 ); i++ ) { QUrl url = m_urlsToLoad.takeFirst(); QString groups = loadedPlaylistsConfig().readEntry( url.url() ); Playlists::PlaylistFilePtr playlist = Playlists::loadPlaylistFile( url, this ); if( !playlist ) { Amarok::Logger::longMessage( i18n("The playlist file \"%1\" could not be loaded.", url.fileName() ), Amarok::Logger::Error ); continue; } if( !groups.isEmpty() && playlist->isWritable() ) - playlist->setGroups( groups.split( ',', QString::SkipEmptyParts ) ); + playlist->setGroups( groups.split( QLatin1Char(','), QString::SkipEmptyParts ) ); m_playlists << playlist; Q_EMIT playlistAdded( PlaylistPtr( playlist.data() ) ); } //give the mainloop time to run if( !m_urlsToLoad.isEmpty() ) QTimer::singleShot( 0, this, &PlaylistFileProvider::loadPlaylists ); } void PlaylistFileProvider::saveLater( Playlists::PlaylistFilePtr playlist ) { //WARNING: this assumes the playlistfile uses it's m_url for uidUrl if( playlist->uidUrl().isEmpty() ) return; if( !m_saveLaterPlaylists.contains( playlist ) ) m_saveLaterPlaylists << playlist; if( !m_saveLaterTimer ) { m_saveLaterTimer = new QTimer( this ); m_saveLaterTimer->setSingleShot( true ); m_saveLaterTimer->setInterval( 0 ); connect( m_saveLaterTimer, &QTimer::timeout, this, &PlaylistFileProvider::slotSaveLater ); } m_saveLaterTimer->start(); } void PlaylistFileProvider::slotSaveLater() //SLOT { foreach( Playlists::PlaylistFilePtr playlist, m_saveLaterPlaylists ) { playlist->save( true ); //TODO: read relative type when loading } m_saveLaterPlaylists.clear(); } KConfigGroup PlaylistFileProvider::loadedPlaylistsConfig() const { return Amarok::config( QStringLiteral("Loaded Playlist Files") ); } } //namespace Playlists diff --git a/src/services/gpodder/GpodderTreeItem.h b/src/services/gpodder/GpodderTreeItem.h index a85bdfc13c..12231a7c14 100644 --- a/src/services/gpodder/GpodderTreeItem.h +++ b/src/services/gpodder/GpodderTreeItem.h @@ -1,60 +1,60 @@ /**************************************************************************************** * Copyright (c) 2011 Stefan Derkits * * Copyright (c) 2011 Christian Wagner * * Copyright (c) 2011 Felix Winter * * * * 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 . * ****************************************************************************************/ #ifndef GPODDERTREEITEM_H_ #define GPODDERTREEITEM_H_ #include #include #include #include #include class GpodderServiceModel; class GpodderTreeItem : public QObject { Q_OBJECT public: - explicit GpodderTreeItem( GpodderTreeItem *parent = nullptr, const QString &name = "" ); + explicit GpodderTreeItem( GpodderTreeItem *parent = nullptr, const QString &name = QString() ); virtual ~GpodderTreeItem(); void appendChild( GpodderTreeItem *child ); GpodderTreeItem *child( int row ); int childCount() const; void setHasChildren( bool hasChildren ); bool hasChildren() const; GpodderTreeItem *parent() const; bool isRoot() const; virtual QVariant displayData() const; virtual void appendTags( mygpo::TagListPtr tags ); virtual void appendPodcasts( mygpo::PodcastListPtr podcasts ); private: QList m_childItems; GpodderTreeItem *m_parentItem; QString m_name; bool m_hasChildren; }; #endif /* GPODDERTREEITEM_H_ */