File indexing completed on 2025-01-19 04:24:37

0001 /****************************************************************************************
0002  * Copyright (c) 2011 Bart Cerneels <bart.cerneels@kde.org>                             *
0003  *                                                                                      *
0004  * This program is free software; you can redistribute it and/or modify it under        *
0005  * the terms of the GNU General Public License as published by the Free Software        *
0006  * Foundation; either version 2 of the License, or (at your option) any later           *
0007  * version.                                                                             *
0008  *                                                                                      *
0009  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0010  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0011  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0012  *                                                                                      *
0013  * You should have received a copy of the GNU General Public License along with         *
0014  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0015  ****************************************************************************************/
0016 
0017 #define DEBUG_PREFIX "UmsCollection"
0018 
0019 #include "UmsCollection.h"
0020 
0021 #include "amarokconfig.h"
0022 #include "ui_UmsConfiguration.h"
0023 #include "collectionscanner/Track.h"
0024 #include "core/capabilities/ActionsCapability.h"
0025 #include "core/logger/Logger.h"
0026 #include "core/meta/Meta.h"
0027 #include "core/support/Components.h"
0028 #include "core/support/Debug.h"
0029 #include "core-impl/collections/support/MemoryQueryMaker.h"
0030 #include "core-impl/collections/support/MemoryMeta.h"
0031 #include "core-impl/collections/umscollection/UmsCollectionLocation.h"
0032 #include "core-impl/collections/umscollection/UmsTranscodeCapability.h"
0033 #include "core-impl/meta/file/File.h"
0034 #include "dialogs/OrganizeCollectionDialog.h"
0035 #include "dialogs/TrackOrganizer.h" //TODO: move to core/utils
0036 #include "scanner/GenericScanManager.h"
0037 
0038 #include <Solid/DeviceInterface>
0039 #include <Solid/DeviceNotifier>
0040 #include <Solid/GenericInterface>
0041 #include <Solid/OpticalDisc>
0042 #include <Solid/PortableMediaPlayer>
0043 #include <Solid/StorageAccess>
0044 #include <Solid/StorageDrive>
0045 #include <Solid/StorageVolume>
0046 
0047 #include <QThread>
0048 #include <QTimer>
0049 #include <QUrl>
0050 
0051 #include <KConfigGroup>
0052 #include <KDiskFreeSpaceInfo>
0053 
0054 
0055 UmsCollectionFactory::UmsCollectionFactory()
0056     : CollectionFactory()
0057 {}
0058 
0059 UmsCollectionFactory::~UmsCollectionFactory()
0060 {
0061 }
0062 
0063 void
0064 UmsCollectionFactory::init()
0065 {
0066     connect( Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded,
0067              this, &UmsCollectionFactory::slotAddSolidDevice );
0068     connect( Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved,
0069              this, &UmsCollectionFactory::slotRemoveSolidDevice );
0070 
0071     // detect UMS devices that were already connected on startup
0072     QString query( "IS StorageAccess" );
0073     QList<Solid::Device> devices = Solid::Device::listFromQuery( query );
0074     foreach( const Solid::Device &device, devices )
0075     {
0076         if( identifySolidDevice( device.udi() ) )
0077             createCollectionForSolidDevice( device.udi() );
0078     }
0079     m_initialized = true;
0080 }
0081 
0082 void
0083 UmsCollectionFactory::slotAddSolidDevice( const QString &udi )
0084 {
0085     if( m_collectionMap.contains( udi ) )
0086         return; // a device added twice (?)
0087 
0088     if( identifySolidDevice( udi ) )
0089         createCollectionForSolidDevice( udi );
0090 }
0091 
0092 void
0093 UmsCollectionFactory::slotAccessibilityChanged( bool accessible, const QString &udi )
0094 {
0095     if( accessible )
0096         slotAddSolidDevice( udi );
0097     else
0098         slotRemoveSolidDevice( udi );
0099 }
0100 
0101 void
0102 UmsCollectionFactory::slotRemoveSolidDevice( const QString &udi )
0103 {
0104     UmsCollection *collection = m_collectionMap.take( udi );
0105     if( collection )
0106         collection->slotDestroy();
0107 }
0108 
0109 void
0110 UmsCollectionFactory::slotRemoveAndTeardownSolidDevice( const QString &udi )
0111 {
0112     UmsCollection *collection = m_collectionMap.take( udi );
0113     if( collection )
0114         collection->slotEject();
0115 }
0116 
0117 void
0118 UmsCollectionFactory::slotCollectionDestroyed( QObject *collection )
0119 {
0120     // remove destroyed collection from m_collectionMap
0121     QMutableMapIterator<QString, UmsCollection *> it( m_collectionMap );
0122     while( it.hasNext() )
0123     {
0124         it.next();
0125         if( (QObject *) it.value() == collection )
0126             it.remove();
0127     }
0128 }
0129 
0130 bool
0131 UmsCollectionFactory::identifySolidDevice( const QString &udi ) const
0132 {
0133     Solid::Device device( udi );
0134     if( !device.is<Solid::StorageAccess>() )
0135         return false;
0136     // HACK to exclude iPods until UMS and iPod have common collection factory
0137     if( device.vendor().contains( "Apple", Qt::CaseInsensitive ) )
0138         return false;
0139 
0140     // everything okay, check whether the device is a data CD
0141     if( device.is<Solid::OpticalDisc>() )
0142     {
0143         const Solid::OpticalDisc *disc = device.as<Solid::OpticalDisc>();
0144         if( disc && ( disc->availableContent() & Solid::OpticalDisc::Data ) )
0145             return true;
0146         return false;
0147     }
0148 
0149     // check whether there is parent USB StorageDrive device
0150     while( device.isValid() )
0151     {
0152         if( device.is<Solid::StorageDrive>() )
0153         {
0154             Solid::StorageDrive *sd = device.as<Solid::StorageDrive>();
0155             if( sd->driveType() == Solid::StorageDrive::CdromDrive )
0156                 return false;
0157             // USB Flash discs are usually hotpluggable, SD/MMC card slots are usually removable
0158             return sd->isHotpluggable() || sd->isRemovable();
0159         }
0160         device = device.parent();
0161     }
0162     return false; // no valid parent USB StorageDrive
0163 }
0164 
0165 void
0166 UmsCollectionFactory::createCollectionForSolidDevice( const QString &udi )
0167 {
0168     DEBUG_BLOCK
0169     Solid::Device device( udi );
0170     Solid::StorageAccess *ssa = device.as<Solid::StorageAccess>();
0171     if( !ssa )
0172     {
0173         warning() << __PRETTY_FUNCTION__ << "called for non-StorageAccess device!?!";
0174         return;
0175     }
0176     if( ssa->isIgnored() )
0177     {
0178         debug() << "device" << udi << "ignored, ignoring :-)";
0179         return;
0180     }
0181 
0182     // we are definitely interested in this device, listen for accessibility changes
0183     disconnect( ssa, &Solid::StorageAccess::accessibilityChanged, this, nullptr );
0184     connect( ssa, &Solid::StorageAccess::accessibilityChanged,
0185              this, &UmsCollectionFactory::slotAccessibilityChanged );
0186 
0187     if( !ssa->isAccessible() )
0188     {
0189         debug() << "device" << udi << "not accessible, ignoring for now";
0190         return;
0191     }
0192 
0193     UmsCollection *collection = new UmsCollection( device );
0194     m_collectionMap.insert( udi, collection );
0195 
0196     // when the collection is destroyed by someone else, remove it from m_collectionMap:
0197     connect( collection, &QObject::destroyed, this, &UmsCollectionFactory::slotCollectionDestroyed );
0198 
0199     // try to gracefully destroy collection when unmounting is requested using
0200     // external means: (Device notifier plasmoid etc.). Because the original action could
0201     // fail if we hold some files on the device open, we try to tearDown the device too.
0202     connect( ssa, &Solid::StorageAccess::teardownRequested, this, &UmsCollectionFactory::slotRemoveAndTeardownSolidDevice );
0203 
0204     Q_EMIT newCollection( collection );
0205 }
0206 
0207 //UmsCollection
0208 
0209 QString UmsCollection::s_settingsFileName( ".is_audio_player" );
0210 QString UmsCollection::s_musicFolderKey( "audio_folder" );
0211 QString UmsCollection::s_musicFilenameSchemeKey( "music_filenamescheme" );
0212 QString UmsCollection::s_vfatSafeKey( "vfat_safe" );
0213 QString UmsCollection::s_asciiOnlyKey( "ascii_only" );
0214 QString UmsCollection::s_postfixTheKey( "ignore_the" );
0215 QString UmsCollection::s_replaceSpacesKey( "replace_spaces" );
0216 QString UmsCollection::s_regexTextKey( "regex_text" );
0217 QString UmsCollection::s_replaceTextKey( "replace_text" );
0218 QString UmsCollection::s_podcastFolderKey( "podcast_folder" );
0219 QString UmsCollection::s_autoConnectKey( "use_automatically" );
0220 QString UmsCollection::s_collectionName( "collection_name" );
0221 QString UmsCollection::s_transcodingGroup( "transcoding" );
0222 
0223 UmsCollection::UmsCollection( const Solid::Device &device )
0224     : Collection()
0225     , m_device( device )
0226     , m_mc( nullptr )
0227     , m_tracksParsed( false )
0228     , m_autoConnect( false )
0229     , m_musicFilenameScheme( "%artist%/%album%/%track% %title%" )
0230     , m_vfatSafe( true )
0231     , m_asciiOnly( false )
0232     , m_postfixThe( false )
0233     , m_replaceSpaces( false )
0234     , m_regexText( QString() )
0235     , m_replaceText( QString() )
0236     , m_collectionName( QString() )
0237     , m_scanManager( nullptr )
0238     , m_lastUpdated( 0 )
0239 {
0240     debug() << "Creating UmsCollection for device with udi: " << m_device.udi();
0241 
0242     m_updateTimer.setSingleShot( true );
0243     connect( this, &UmsCollection::startUpdateTimer, this, &UmsCollection::slotStartUpdateTimer );
0244     connect( &m_updateTimer, &QTimer::timeout, this, &UmsCollection::collectionUpdated );
0245 
0246     m_configureAction = new QAction( QIcon::fromTheme( "configure" ), i18n( "&Configure Device" ), this );
0247     m_configureAction->setProperty( "popupdropper_svg_id", "configure" );
0248     connect( m_configureAction, &QAction::triggered, this, &UmsCollection::slotConfigure );
0249 
0250     m_parseAction = new QAction( QIcon::fromTheme( "checkbox" ), i18n(  "&Activate This Collection" ), this );
0251     m_parseAction->setProperty( "popupdropper_svg_id", "edit" );
0252     connect( m_parseAction, &QAction::triggered, this, &UmsCollection::slotParseActionTriggered );
0253 
0254     m_ejectAction = new QAction( QIcon::fromTheme( "media-eject" ), i18n( "&Eject Device" ),
0255                                  const_cast<UmsCollection*>( this ) );
0256     m_ejectAction->setProperty( "popupdropper_svg_id", "eject" );
0257     connect( m_ejectAction, &QAction::triggered, this, &UmsCollection::slotEject );
0258 
0259     init();
0260 }
0261 
0262 UmsCollection::~UmsCollection()
0263 {
0264     DEBUG_BLOCK
0265 }
0266 
0267 void
0268 UmsCollection::init()
0269 {
0270     Solid::StorageAccess *storageAccess = m_device.as<Solid::StorageAccess>();
0271     m_mountPoint = storageAccess->filePath();
0272     Solid::StorageVolume *ssv = m_device.as<Solid::StorageVolume>();
0273     m_collectionId = ssv ? ssv->uuid() : m_device.udi();
0274     debug() << "Mounted at: " << m_mountPoint << "collection id:" << m_collectionId;
0275 
0276     // read .is_audio_player from filesystem
0277     KConfig config( m_mountPoint + QLatin1Char('/') + s_settingsFileName, KConfig::SimpleConfig );
0278     KConfigGroup entries = config.group( QString() ); // default group
0279     if( entries.hasKey( s_musicFolderKey ) )
0280     {
0281         m_musicUrl = QUrl::fromLocalFile( m_mountPoint );
0282         m_musicUrl = m_musicUrl.adjusted(QUrl::StripTrailingSlash);
0283         m_musicUrl.setPath(m_musicUrl.path() + QLatin1Char('/') + ( entries.readPathEntry( s_musicFolderKey, QString() ) ));
0284         m_musicUrl.setPath( QDir::cleanPath(m_musicUrl.path()) );
0285         if( !QDir( m_musicUrl.toLocalFile() ).exists() )
0286         {
0287             QString message = i18n( "File <i>%1</i> suggests that we should use <i>%2</i> "
0288                     "as music folder on the device, but it doesn't exist. Falling back to "
0289                     "<i>%3</i> instead", m_mountPoint + QLatin1Char('/') + s_settingsFileName,
0290                     m_musicUrl.toLocalFile(), m_mountPoint );
0291             Amarok::Logger::longMessage( message, Amarok::Logger::Warning );
0292             m_musicUrl = QUrl::fromLocalFile(m_mountPoint);
0293         }
0294     }
0295     else if( !entries.keyList().isEmpty() )
0296         // config file exists, but has no s_musicFolderKey -> music should be disabled
0297         m_musicUrl = QUrl();
0298     else
0299         m_musicUrl = QUrl::fromLocalFile(m_mountPoint); // related BR 259849
0300     QString scheme = entries.readEntry( s_musicFilenameSchemeKey );
0301     m_musicFilenameScheme = !scheme.isEmpty() ? scheme : m_musicFilenameScheme;
0302     m_vfatSafe = entries.readEntry( s_vfatSafeKey, m_vfatSafe );
0303     m_asciiOnly = entries.readEntry( s_asciiOnlyKey, m_asciiOnly );
0304     m_postfixThe = entries.readEntry( s_postfixTheKey, m_postfixThe );
0305     m_replaceSpaces = entries.readEntry( s_replaceSpacesKey, m_replaceSpaces );
0306     m_regexText = entries.readEntry( s_regexTextKey, m_regexText );
0307     m_replaceText = entries.readEntry( s_replaceTextKey, m_replaceText );
0308     if( entries.hasKey( s_podcastFolderKey ) )
0309     {
0310         m_podcastUrl = QUrl::fromLocalFile( m_mountPoint );
0311         m_podcastUrl = m_podcastUrl.adjusted(QUrl::StripTrailingSlash);
0312         m_podcastUrl.setPath(m_podcastUrl.path() + QLatin1Char('/') + ( entries.readPathEntry( s_podcastFolderKey, QString() ) ));
0313         m_podcastUrl.setPath( QDir::cleanPath(m_podcastUrl.path()) );
0314     }
0315     m_autoConnect = entries.readEntry( s_autoConnectKey, m_autoConnect );
0316     m_collectionName = entries.readEntry( s_collectionName, m_collectionName );
0317 
0318     m_mc = QSharedPointer<MemoryCollection>(new MemoryCollection());
0319 
0320     if( m_autoConnect )
0321         QTimer::singleShot( 0, this, &UmsCollection::slotParseTracks );
0322 }
0323 
0324 bool
0325 UmsCollection::possiblyContainsTrack( const QUrl &url ) const
0326 {
0327     //not initialized yet.
0328     if( m_mc.isNull() )
0329         return false;
0330 
0331     QString u = QUrl::fromPercentEncoding( url.url().toUtf8() );
0332     return u.startsWith( m_mountPoint ) || u.startsWith( "file://" + m_mountPoint );
0333 }
0334 
0335 Meta::TrackPtr
0336 UmsCollection::trackForUrl( const QUrl &url )
0337 {
0338     //not initialized yet.
0339     if( m_mc.isNull() )
0340         return Meta::TrackPtr();
0341 
0342     QString uid = QUrl::fromPercentEncoding( url.url().toUtf8() );
0343     if( uid.startsWith("file://") )
0344         uid = uid.remove( 0, 7 );
0345     return m_mc->trackMap().value( uid, Meta::TrackPtr() );
0346 }
0347 
0348 QueryMaker *
0349 UmsCollection::queryMaker()
0350 {
0351     return new MemoryQueryMaker( m_mc.toWeakRef(), collectionId() );
0352 }
0353 
0354 QString
0355 UmsCollection::uidUrlProtocol() const
0356 {
0357     return QStringLiteral( "file://" );
0358 }
0359 
0360 QString
0361 UmsCollection::collectionId() const
0362 {
0363     return m_collectionId;
0364 }
0365 
0366 QString
0367 UmsCollection::prettyName() const
0368 {
0369     QString actualName;
0370     if( !m_collectionName.isEmpty() )
0371         actualName = m_collectionName;
0372     else if( !m_device.description().isEmpty() )
0373         actualName = m_device.description();
0374     else
0375     {
0376         actualName = m_device.vendor().simplified();
0377         if( !actualName.isEmpty() )
0378             actualName += ' ';
0379         actualName += m_device.product().simplified();
0380     }
0381 
0382     if( m_tracksParsed )
0383         return actualName;
0384     else
0385         return i18nc( "Name of the USB Mass Storage collection that has not yet been "
0386                       "activated. See also the 'Activate This Collection' action; %1 is "
0387                       "actual collection name", "%1 (not activated)", actualName );
0388 }
0389 
0390 QIcon
0391 UmsCollection::icon() const
0392 {
0393     if( m_device.icon().isEmpty() )
0394         return QIcon::fromTheme( "drive-removable-media-usb-pendrive" );
0395     else
0396         return QIcon::fromTheme( m_device.icon() );
0397 }
0398 
0399 bool
0400 UmsCollection::hasCapacity() const
0401 {
0402     if( m_device.isValid() && m_device.is<Solid::StorageAccess>() )
0403         return m_device.as<Solid::StorageAccess>()->isAccessible();
0404     return false;
0405 }
0406 
0407 float
0408 UmsCollection::usedCapacity() const
0409 {
0410     return KDiskFreeSpaceInfo::freeSpaceInfo( m_mountPoint ).used();
0411 }
0412 
0413 float
0414 UmsCollection::totalCapacity() const
0415 {
0416     return KDiskFreeSpaceInfo::freeSpaceInfo( m_mountPoint ).size();
0417 }
0418 
0419 CollectionLocation *
0420 UmsCollection::location()
0421 {
0422     return new UmsCollectionLocation( this );
0423 }
0424 
0425 bool
0426 UmsCollection::isOrganizable() const
0427 {
0428     return isWritable();
0429 }
0430 
0431 bool
0432 UmsCollection::hasCapabilityInterface( Capabilities::Capability::Type type ) const
0433 {
0434     switch( type )
0435     {
0436         case Capabilities::Capability::Actions:
0437         case Capabilities::Capability::Transcode:
0438             return true;
0439         default:
0440             return false;
0441     }
0442 }
0443 
0444 Capabilities::Capability *
0445 UmsCollection::createCapabilityInterface( Capabilities::Capability::Type type )
0446 {
0447     switch( type )
0448     {
0449         case Capabilities::Capability::Actions:
0450         {
0451             QList<QAction *> actions;
0452             if( m_tracksParsed )
0453             {
0454                 actions << m_configureAction;
0455                 actions << m_ejectAction;
0456             }
0457             else
0458             {
0459                 actions << m_parseAction;
0460             }
0461             return new Capabilities::ActionsCapability( actions );
0462         }
0463         case Capabilities::Capability::Transcode:
0464             return new UmsTranscodeCapability( m_mountPoint + QLatin1Char('/') + s_settingsFileName,
0465                                                s_transcodingGroup );
0466         default:
0467             return nullptr;
0468     }
0469 }
0470 
0471 void
0472 UmsCollection::metadataChanged(const Meta::TrackPtr &track )
0473 {
0474     if( MemoryMeta::MapChanger( m_mc.data() ).trackChanged( track ) )
0475         // big-enough change:
0476         Q_EMIT startUpdateTimer();
0477 }
0478 
0479 QUrl
0480 UmsCollection::organizedUrl( const Meta::TrackPtr &track, const QString &fileExtension ) const
0481 {
0482     TrackOrganizer trackOrganizer( Meta::TrackList() << track );
0483     //%folder% prefix required to get absolute url.
0484     trackOrganizer.setFormatString( "%collectionroot%/" + m_musicFilenameScheme + ".%filetype%" );
0485     trackOrganizer.setVfatSafe( m_vfatSafe );
0486     trackOrganizer.setAsciiOnly( m_asciiOnly );
0487     trackOrganizer.setFolderPrefix( m_musicUrl.path() );
0488     trackOrganizer.setPostfixThe( m_postfixThe );
0489     trackOrganizer.setReplaceSpaces( m_replaceSpaces );
0490     trackOrganizer.setReplace( m_regexText, m_replaceText );
0491     if( !fileExtension.isEmpty() )
0492         trackOrganizer.setTargetFileExtension( fileExtension );
0493 
0494     return QUrl::fromLocalFile( trackOrganizer.getDestinations().value( track ) );
0495 }
0496 
0497 void
0498 UmsCollection::slotDestroy()
0499 {
0500     //TODO: stop scanner if running
0501     //unregister PlaylistProvider
0502     //CollectionManager will call destructor.
0503     Q_EMIT remove();
0504 }
0505 
0506 void
0507 UmsCollection::slotEject()
0508 {
0509     slotDestroy();
0510     Solid::StorageAccess *storageAccess = m_device.as<Solid::StorageAccess>();
0511     storageAccess->teardown();
0512 }
0513 
0514 void
0515 UmsCollection::slotTrackAdded( const QUrl &location )
0516 {
0517     Q_ASSERT( m_musicUrl.isParentOf( location ) || m_musicUrl.matches( location , QUrl::StripTrailingSlash) );
0518     MetaFile::Track *fileTrack = new MetaFile::Track( location );
0519     fileTrack->setCollection( this );
0520     Meta::TrackPtr fileTrackPtr = Meta::TrackPtr( fileTrack );
0521     Meta::TrackPtr proxyTrack = MemoryMeta::MapChanger( m_mc.data() ).addTrack( fileTrackPtr );
0522     if( proxyTrack )
0523     {
0524         subscribeTo( fileTrackPtr );
0525         Q_EMIT startUpdateTimer();
0526     }
0527     else
0528         warning() << __PRETTY_FUNCTION__ << "Failed to add" << fileTrackPtr->playableUrl()
0529                   << "to MemoryCollection. Perhaps already there?!?";
0530 }
0531 
0532 void
0533 UmsCollection::slotTrackRemoved( const Meta::TrackPtr &track )
0534 {
0535     Meta::TrackPtr removedTrack = MemoryMeta::MapChanger( m_mc.data() ).removeTrack( track );
0536     if( removedTrack )
0537     {
0538         unsubscribeFrom( removedTrack );
0539         // we only added MetaFile::Tracks, following static cast is safe
0540         static_cast<MetaFile::Track*>( removedTrack.data() )->setCollection( nullptr );
0541         Q_EMIT startUpdateTimer();
0542     }
0543     else
0544         warning() << __PRETTY_FUNCTION__ << "Failed to remove" << track->playableUrl()
0545                   << "from MemoryCollection. Perhaps it was never there?";
0546 }
0547 
0548 void
0549 UmsCollection::collectionUpdated()
0550 {
0551     m_lastUpdated = QDateTime::currentMSecsSinceEpoch();
0552     Q_EMIT updated();
0553 }
0554 
0555 void
0556 UmsCollection::slotParseTracks()
0557 {
0558     if( !m_scanManager )
0559     {
0560         m_scanManager = new GenericScanManager( this );
0561         connect( m_scanManager, &GenericScanManager::directoryScanned,
0562                  this, &UmsCollection::slotDirectoryScanned );
0563     }
0564 
0565     m_tracksParsed = true;
0566     m_scanManager->requestScan( QList<QUrl>() << m_musicUrl, GenericScanManager::FullScan );
0567 }
0568 
0569 void
0570 UmsCollection::slotParseActionTriggered()
0571 {
0572     if( m_mc->trackMap().isEmpty() )
0573         QTimer::singleShot( 0, this, &UmsCollection::slotParseTracks );
0574 }
0575 
0576 void
0577 UmsCollection::slotConfigure()
0578 {
0579     QDialog umsSettingsDialog;
0580     QWidget *settingsWidget = new QWidget( &umsSettingsDialog );
0581     QScopedPointer<Capabilities::TranscodeCapability> tc( create<Capabilities::TranscodeCapability>() );
0582 
0583     Ui::UmsConfiguration *settings = new Ui::UmsConfiguration();
0584     settings->setupUi( settingsWidget );
0585 
0586     settings->m_autoConnect->setChecked( m_autoConnect );
0587 
0588     settings->m_musicFolder->setMode( KFile::Directory );
0589     settings->m_musicCheckBox->setChecked( !m_musicUrl.isEmpty() );
0590     settings->m_musicWidget->setEnabled( settings->m_musicCheckBox->isChecked() );
0591     settings->m_musicFolder->setUrl( m_musicUrl.isEmpty() ? QUrl::fromLocalFile( m_mountPoint ) : m_musicUrl );
0592     settings->m_transcodeConfig->fillInChoices( tc->savedConfiguration() );
0593 
0594     settings->m_podcastFolder->setMode( KFile::Directory );
0595     settings->m_podcastCheckBox->setChecked( !m_podcastUrl.isEmpty() );
0596     settings->m_podcastWidget->setEnabled( settings->m_podcastCheckBox->isChecked() );
0597     settings->m_podcastFolder->setUrl( m_podcastUrl.isEmpty() ? QUrl::fromLocalFile( m_mountPoint )
0598                                          : m_podcastUrl );
0599 
0600     settings->m_collectionName->setText( prettyName() );
0601 
0602     OrganizeCollectionWidget *layoutWidget = new OrganizeCollectionWidget;
0603     //TODO: save the setting that are normally written in onAccept()
0604 //    connect( this, SIGNAL(accepted()), &layoutWidget, SLOT(onAccept()) );
0605     QVBoxLayout *layout = new QVBoxLayout;
0606     layout->addWidget( layoutWidget );
0607     settings->m_filenameSchemeBox->setLayout( layout );
0608     //hide the unuse preset selector.
0609     //TODO: change the presets to concurrent presets for regular albums v.s. compilations
0610     // layoutWidget.setformatPresetVisible( false );
0611     layoutWidget->setScheme( m_musicFilenameScheme );
0612 
0613     OrganizeCollectionOptionWidget *optionsWidget = new OrganizeCollectionOptionWidget;
0614     optionsWidget->setVfatCompatible( m_vfatSafe );
0615     optionsWidget->setAsciiOnly( m_asciiOnly );
0616     optionsWidget->setPostfixThe( m_postfixThe );
0617     optionsWidget->setReplaceSpaces( m_replaceSpaces );
0618     optionsWidget->setRegexpText( m_regexText );
0619     optionsWidget->setReplaceText( m_replaceText );
0620 
0621     layout->addWidget( optionsWidget );
0622 
0623     umsSettingsDialog.setLayout( new QVBoxLayout );
0624     QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel );
0625     connect( buttonBox, &QDialogButtonBox::accepted, &umsSettingsDialog, &QDialog::accept );
0626     connect( buttonBox, &QDialogButtonBox::rejected, &umsSettingsDialog, &QDialog::reject );
0627     umsSettingsDialog.layout()->addWidget( settingsWidget );
0628     umsSettingsDialog.layout()->addWidget( buttonBox );
0629     umsSettingsDialog.setWindowTitle( i18n( "Configure USB Mass Storage Device" ) );
0630 
0631     if( umsSettingsDialog.exec() == QDialog::Accepted )
0632     {
0633         debug() << "accepted";
0634 
0635         if( settings->m_musicCheckBox->isChecked() )
0636         {
0637             if( settings->m_musicFolder->url() != m_musicUrl )
0638             {
0639                 debug() << "music location changed from " << m_musicUrl.toLocalFile() << " to ";
0640                 debug() << settings->m_musicFolder->url().toLocalFile();
0641                 m_musicUrl = settings->m_musicFolder->url();
0642                 //TODO: reparse music
0643             }
0644             QString scheme = layoutWidget->getParsableScheme().simplified();
0645             //protect against empty string.
0646             if( !scheme.isEmpty() )
0647                 m_musicFilenameScheme = scheme;
0648         }
0649         else
0650         {
0651             debug() << "music support is disabled";
0652             m_musicUrl = QUrl();
0653             //TODO: remove all tracks from the MemoryCollection.
0654         }
0655 
0656         m_asciiOnly = optionsWidget->asciiOnly();
0657         m_postfixThe = optionsWidget->postfixThe();
0658         m_replaceSpaces = optionsWidget->replaceSpaces();
0659         m_regexText = optionsWidget->regexpText();
0660         m_replaceText = optionsWidget->replaceText();
0661         m_collectionName = settings->m_collectionName->text();
0662 
0663         if( settings->m_podcastCheckBox->isChecked() )
0664         {
0665             if( settings->m_podcastFolder->url() != m_podcastUrl )
0666             {
0667                 debug() << "podcast location changed from " << m_podcastUrl << " to ";
0668                 debug() << settings->m_podcastFolder->url().url();
0669                 m_podcastUrl = QUrl(settings->m_podcastFolder->url());
0670                 //TODO: reparse podcasts
0671             }
0672         }
0673         else
0674         {
0675             debug() << "podcast support is disabled";
0676             m_podcastUrl = QUrl();
0677             //TODO: remove the PodcastProvider
0678         }
0679 
0680         m_autoConnect = settings->m_autoConnect->isChecked();
0681         if( !m_musicUrl.isEmpty() && m_autoConnect )
0682             QTimer::singleShot( 0, this, &UmsCollection::slotParseTracks );
0683 
0684         // write the data to the on-disk file
0685         KConfig config( m_mountPoint + QLatin1Char('/') + s_settingsFileName, KConfig::SimpleConfig );
0686         KConfigGroup entries = config.group( QString() ); // default group
0687         if( !m_musicUrl.isEmpty() )
0688             entries.writePathEntry( s_musicFolderKey, QDir( m_mountPoint ).relativeFilePath( m_musicUrl.toLocalFile() ));
0689         else
0690             entries.deleteEntry( s_musicFolderKey );
0691         entries.writeEntry( s_musicFilenameSchemeKey, m_musicFilenameScheme );
0692         entries.writeEntry( s_vfatSafeKey, m_vfatSafe );
0693         entries.writeEntry( s_asciiOnlyKey, m_asciiOnly );
0694         entries.writeEntry( s_postfixTheKey, m_postfixThe );
0695         entries.writeEntry( s_replaceSpacesKey, m_replaceSpaces );
0696         entries.writeEntry( s_regexTextKey, m_regexText );
0697         entries.writeEntry( s_replaceTextKey, m_replaceText );
0698         if( !m_podcastUrl.isEmpty() )
0699             entries.writePathEntry( s_podcastFolderKey, QDir( m_mountPoint ).relativeFilePath( m_podcastUrl.toLocalFile() ));
0700         else
0701             entries.deleteEntry( s_podcastFolderKey );
0702         entries.writeEntry( s_autoConnectKey, m_autoConnect );
0703         entries.writeEntry( s_collectionName, m_collectionName );
0704         config.sync();
0705 
0706         tc->setSavedConfiguration( settings->m_transcodeConfig->currentChoice() );
0707     }
0708 
0709     delete settings;
0710 }
0711 
0712 void
0713 UmsCollection::slotDirectoryScanned( QSharedPointer<CollectionScanner::Directory> dir )
0714 {
0715     debug() << "directory scanned: " << dir->path();
0716     if( dir->tracks().isEmpty() )
0717     {
0718         debug() << "does not have tracks";
0719         return;
0720     }
0721 
0722     foreach( const CollectionScanner::Track *scannerTrack, dir->tracks() )
0723     {
0724         //TODO: use proxy tracks so no real file read is required
0725         // following method calls startUpdateTimer(), no need to Q_EMIT updated()
0726         slotTrackAdded( QUrl::fromLocalFile(scannerTrack->path()) );
0727     }
0728 
0729     //TODO: read playlists
0730 }
0731 
0732 void
0733 UmsCollection::slotStartUpdateTimer()
0734 {
0735     // there are no concurrency problems, this method can only be called from the main
0736     // thread and that's where the timer fires
0737     if( m_updateTimer.isActive() )
0738         return; // already running, nothing to do
0739 
0740     // number of milliseconds to next desired update, may be negative
0741     int timeout = m_lastUpdated + 1000 - QDateTime::currentMSecsSinceEpoch();
0742     // give at least 50 msecs to catch multi-tracks edits nicely on the first frame
0743     m_updateTimer.start( qBound( 50, timeout, 1000 ) );
0744 }