1 /****************************************************************************************
2  * Copyright (c) 2011 Bart Cerneels <bart.cerneels@kde.org>                             *
3  *                                                                                      *
4  * This program is free software; you can redistribute it and/or modify it under        *
5  * the terms of the GNU General Public License as published by the Free Software        *
6  * Foundation; either version 2 of the License, or (at your option) any later           *
7  * version.                                                                             *
8  *                                                                                      *
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
11  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
12  *                                                                                      *
13  * You should have received a copy of the GNU General Public License along with         *
14  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
15  ****************************************************************************************/
16 
17 #define DEBUG_PREFIX "UmsCollection"
18 
19 #include "UmsCollection.h"
20 
21 #include "amarokconfig.h"
22 #include "ui_UmsConfiguration.h"
23 #include "collectionscanner/Track.h"
24 #include "core/capabilities/ActionsCapability.h"
25 #include "core/logger/Logger.h"
26 #include "core/meta/Meta.h"
27 #include "core/support/Components.h"
28 #include "core/support/Debug.h"
29 #include "core-impl/collections/support/MemoryQueryMaker.h"
30 #include "core-impl/collections/support/MemoryMeta.h"
31 #include "core-impl/collections/umscollection/UmsCollectionLocation.h"
32 #include "core-impl/collections/umscollection/UmsTranscodeCapability.h"
33 #include "core-impl/meta/file/File.h"
34 #include "dialogs/OrganizeCollectionDialog.h"
35 #include "dialogs/TrackOrganizer.h" //TODO: move to core/utils
36 #include "scanner/GenericScanManager.h"
37 
38 #include <Solid/DeviceInterface>
39 #include <Solid/DeviceNotifier>
40 #include <Solid/GenericInterface>
41 #include <Solid/OpticalDisc>
42 #include <Solid/PortableMediaPlayer>
43 #include <Solid/StorageAccess>
44 #include <Solid/StorageDrive>
45 #include <Solid/StorageVolume>
46 
47 #include <QThread>
48 #include <QTimer>
49 #include <QUrl>
50 
51 #include <KConfigGroup>
52 #include <KDiskFreeSpaceInfo>
53 
54 
UmsCollectionFactory()55 UmsCollectionFactory::UmsCollectionFactory()
56     : CollectionFactory()
57 {}
58 
~UmsCollectionFactory()59 UmsCollectionFactory::~UmsCollectionFactory()
60 {
61 }
62 
63 void
init()64 UmsCollectionFactory::init()
65 {
66     connect( Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded,
67              this, &UmsCollectionFactory::slotAddSolidDevice );
68     connect( Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved,
69              this, &UmsCollectionFactory::slotRemoveSolidDevice );
70 
71     // detect UMS devices that were already connected on startup
72     QString query( "IS StorageAccess" );
73     QList<Solid::Device> devices = Solid::Device::listFromQuery( query );
74     foreach( const Solid::Device &device, devices )
75     {
76         if( identifySolidDevice( device.udi() ) )
77             createCollectionForSolidDevice( device.udi() );
78     }
79     m_initialized = true;
80 }
81 
82 void
slotAddSolidDevice(const QString & udi)83 UmsCollectionFactory::slotAddSolidDevice( const QString &udi )
84 {
85     if( m_collectionMap.contains( udi ) )
86         return; // a device added twice (?)
87 
88     if( identifySolidDevice( udi ) )
89         createCollectionForSolidDevice( udi );
90 }
91 
92 void
slotAccessibilityChanged(bool accessible,const QString & udi)93 UmsCollectionFactory::slotAccessibilityChanged( bool accessible, const QString &udi )
94 {
95     if( accessible )
96         slotAddSolidDevice( udi );
97     else
98         slotRemoveSolidDevice( udi );
99 }
100 
101 void
slotRemoveSolidDevice(const QString & udi)102 UmsCollectionFactory::slotRemoveSolidDevice( const QString &udi )
103 {
104     UmsCollection *collection = m_collectionMap.take( udi );
105     if( collection )
106         collection->slotDestroy();
107 }
108 
109 void
slotRemoveAndTeardownSolidDevice(const QString & udi)110 UmsCollectionFactory::slotRemoveAndTeardownSolidDevice( const QString &udi )
111 {
112     UmsCollection *collection = m_collectionMap.take( udi );
113     if( collection )
114         collection->slotEject();
115 }
116 
117 void
slotCollectionDestroyed(QObject * collection)118 UmsCollectionFactory::slotCollectionDestroyed( QObject *collection )
119 {
120     // remove destroyed collection from m_collectionMap
121     QMutableMapIterator<QString, UmsCollection *> it( m_collectionMap );
122     while( it.hasNext() )
123     {
124         it.next();
125         if( (QObject *) it.value() == collection )
126             it.remove();
127     }
128 }
129 
130 bool
identifySolidDevice(const QString & udi) const131 UmsCollectionFactory::identifySolidDevice( const QString &udi ) const
132 {
133     Solid::Device device( udi );
134     if( !device.is<Solid::StorageAccess>() )
135         return false;
136     // HACK to exclude iPods until UMS and iPod have common collection factory
137     if( device.vendor().contains( "Apple", Qt::CaseInsensitive ) )
138         return false;
139 
140     // everything okay, check whether the device is a data CD
141     if( device.is<Solid::OpticalDisc>() )
142     {
143         const Solid::OpticalDisc *disc = device.as<Solid::OpticalDisc>();
144         if( disc && ( disc->availableContent() & Solid::OpticalDisc::Data ) )
145             return true;
146         return false;
147     }
148 
149     // check whether there is parent USB StorageDrive device
150     while( device.isValid() )
151     {
152         if( device.is<Solid::StorageDrive>() )
153         {
154             Solid::StorageDrive *sd = device.as<Solid::StorageDrive>();
155             if( sd->driveType() == Solid::StorageDrive::CdromDrive )
156                 return false;
157             // USB Flash discs are usually hotpluggable, SD/MMC card slots are usually removable
158             return sd->isHotpluggable() || sd->isRemovable();
159         }
160         device = device.parent();
161     }
162     return false; // no valid parent USB StorageDrive
163 }
164 
165 void
createCollectionForSolidDevice(const QString & udi)166 UmsCollectionFactory::createCollectionForSolidDevice( const QString &udi )
167 {
168     DEBUG_BLOCK
169     Solid::Device device( udi );
170     Solid::StorageAccess *ssa = device.as<Solid::StorageAccess>();
171     if( !ssa )
172     {
173         warning() << __PRETTY_FUNCTION__ << "called for non-StorageAccess device!?!";
174         return;
175     }
176     if( ssa->isIgnored() )
177     {
178         debug() << "device" << udi << "ignored, ignoring :-)";
179         return;
180     }
181 
182     // we are definitely interested in this device, listen for accessibility changes
183     disconnect( ssa, &Solid::StorageAccess::accessibilityChanged, this, 0 );
184     connect( ssa, &Solid::StorageAccess::accessibilityChanged,
185              this, &UmsCollectionFactory::slotAccessibilityChanged );
186 
187     if( !ssa->isAccessible() )
188     {
189         debug() << "device" << udi << "not accessible, ignoring for now";
190         return;
191     }
192 
193     UmsCollection *collection = new UmsCollection( device );
194     m_collectionMap.insert( udi, collection );
195 
196     // when the collection is destroyed by someone else, remove it from m_collectionMap:
197     connect( collection, &QObject::destroyed, this, &UmsCollectionFactory::slotCollectionDestroyed );
198 
199     // try to gracefully destroy collection when unmounting is requested using
200     // external means: (Device notifier plasmoid etc.). Because the original action could
201     // fail if we hold some files on the device open, we try to tearDown the device too.
202     connect( ssa, &Solid::StorageAccess::teardownRequested, this, &UmsCollectionFactory::slotRemoveAndTeardownSolidDevice );
203 
204     Q_EMIT newCollection( collection );
205 }
206 
207 //UmsCollection
208 
209 QString UmsCollection::s_settingsFileName( ".is_audio_player" );
210 QString UmsCollection::s_musicFolderKey( "audio_folder" );
211 QString UmsCollection::s_musicFilenameSchemeKey( "music_filenamescheme" );
212 QString UmsCollection::s_vfatSafeKey( "vfat_safe" );
213 QString UmsCollection::s_asciiOnlyKey( "ascii_only" );
214 QString UmsCollection::s_postfixTheKey( "ignore_the" );
215 QString UmsCollection::s_replaceSpacesKey( "replace_spaces" );
216 QString UmsCollection::s_regexTextKey( "regex_text" );
217 QString UmsCollection::s_replaceTextKey( "replace_text" );
218 QString UmsCollection::s_podcastFolderKey( "podcast_folder" );
219 QString UmsCollection::s_autoConnectKey( "use_automatically" );
220 QString UmsCollection::s_collectionName( "collection_name" );
221 QString UmsCollection::s_transcodingGroup( "transcoding" );
222 
UmsCollection(const Solid::Device & device)223 UmsCollection::UmsCollection( const Solid::Device &device )
224     : Collection()
225     , m_device( device )
226     , m_mc( 0 )
227     , m_tracksParsed( false )
228     , m_autoConnect( false )
229     , m_musicFilenameScheme( "%artist%/%album%/%track% %title%" )
230     , m_vfatSafe( true )
231     , m_asciiOnly( false )
232     , m_postfixThe( false )
233     , m_replaceSpaces( false )
234     , m_regexText( QString() )
235     , m_replaceText( QString() )
236     , m_collectionName( QString() )
237     , m_scanManager( 0 )
238     , m_lastUpdated( 0 )
239 {
240     debug() << "Creating UmsCollection for device with udi: " << m_device.udi();
241 
242     m_updateTimer.setSingleShot( true );
243     connect( this, &UmsCollection::startUpdateTimer, this, &UmsCollection::slotStartUpdateTimer );
244     connect( &m_updateTimer, &QTimer::timeout, this, &UmsCollection::collectionUpdated );
245 
246     m_configureAction = new QAction( QIcon::fromTheme( "configure" ), i18n( "&Configure Device" ), this );
247     m_configureAction->setProperty( "popupdropper_svg_id", "configure" );
248     connect( m_configureAction, &QAction::triggered, this, &UmsCollection::slotConfigure );
249 
250     m_parseAction = new QAction( QIcon::fromTheme( "checkbox" ), i18n(  "&Activate This Collection" ), this );
251     m_parseAction->setProperty( "popupdropper_svg_id", "edit" );
252     connect( m_parseAction, &QAction::triggered, this, &UmsCollection::slotParseActionTriggered );
253 
254     m_ejectAction = new QAction( QIcon::fromTheme( "media-eject" ), i18n( "&Eject Device" ),
255                                  const_cast<UmsCollection*>( this ) );
256     m_ejectAction->setProperty( "popupdropper_svg_id", "eject" );
257     connect( m_ejectAction, &QAction::triggered, this, &UmsCollection::slotEject );
258 
259     init();
260 }
261 
~UmsCollection()262 UmsCollection::~UmsCollection()
263 {
264     DEBUG_BLOCK
265 }
266 
267 void
init()268 UmsCollection::init()
269 {
270     Solid::StorageAccess *storageAccess = m_device.as<Solid::StorageAccess>();
271     m_mountPoint = storageAccess->filePath();
272     Solid::StorageVolume *ssv = m_device.as<Solid::StorageVolume>();
273     m_collectionId = ssv ? ssv->uuid() : m_device.udi();
274     debug() << "Mounted at: " << m_mountPoint << "collection id:" << m_collectionId;
275 
276     // read .is_audio_player from filesystem
277     KConfig config( m_mountPoint + QLatin1Char('/') + s_settingsFileName, KConfig::SimpleConfig );
278     KConfigGroup entries = config.group( QString() ); // default group
279     if( entries.hasKey( s_musicFolderKey ) )
280     {
281         m_musicUrl = QUrl::fromLocalFile( m_mountPoint );
282         m_musicUrl = m_musicUrl.adjusted(QUrl::StripTrailingSlash);
283         m_musicUrl.setPath(m_musicUrl.path() + QLatin1Char('/') + ( entries.readPathEntry( s_musicFolderKey, QString() ) ));
284         m_musicUrl.setPath( QDir::cleanPath(m_musicUrl.path()) );
285         if( !QDir( m_musicUrl.toLocalFile() ).exists() )
286         {
287             QString message = i18n( "File <i>%1</i> suggests that we should use <i>%2</i> "
288                     "as music folder on the device, but it doesn't exist. Falling back to "
289                     "<i>%3</i> instead", m_mountPoint + QLatin1Char('/') + s_settingsFileName,
290                     m_musicUrl.toLocalFile(), m_mountPoint );
291             Amarok::Logger::longMessage( message, Amarok::Logger::Warning );
292             m_musicUrl = QUrl::fromLocalFile(m_mountPoint);
293         }
294     }
295     else if( !entries.keyList().isEmpty() )
296         // config file exists, but has no s_musicFolderKey -> music should be disabled
297         m_musicUrl = QUrl();
298     else
299         m_musicUrl = QUrl::fromLocalFile(m_mountPoint); // related BR 259849
300     QString scheme = entries.readEntry( s_musicFilenameSchemeKey );
301     m_musicFilenameScheme = !scheme.isEmpty() ? scheme : m_musicFilenameScheme;
302     m_vfatSafe = entries.readEntry( s_vfatSafeKey, m_vfatSafe );
303     m_asciiOnly = entries.readEntry( s_asciiOnlyKey, m_asciiOnly );
304     m_postfixThe = entries.readEntry( s_postfixTheKey, m_postfixThe );
305     m_replaceSpaces = entries.readEntry( s_replaceSpacesKey, m_replaceSpaces );
306     m_regexText = entries.readEntry( s_regexTextKey, m_regexText );
307     m_replaceText = entries.readEntry( s_replaceTextKey, m_replaceText );
308     if( entries.hasKey( s_podcastFolderKey ) )
309     {
310         m_podcastUrl = QUrl::fromLocalFile( m_mountPoint );
311         m_podcastUrl = m_podcastUrl.adjusted(QUrl::StripTrailingSlash);
312         m_podcastUrl.setPath(m_podcastUrl.path() + QLatin1Char('/') + ( entries.readPathEntry( s_podcastFolderKey, QString() ) ));
313         m_podcastUrl.setPath( QDir::cleanPath(m_podcastUrl.path()) );
314     }
315     m_autoConnect = entries.readEntry( s_autoConnectKey, m_autoConnect );
316     m_collectionName = entries.readEntry( s_collectionName, m_collectionName );
317 
318     m_mc = QSharedPointer<MemoryCollection>(new MemoryCollection());
319 
320     if( m_autoConnect )
321         QTimer::singleShot( 0, this, &UmsCollection::slotParseTracks );
322 }
323 
324 bool
possiblyContainsTrack(const QUrl & url) const325 UmsCollection::possiblyContainsTrack( const QUrl &url ) const
326 {
327     //not initialized yet.
328     if( m_mc.isNull() )
329         return false;
330 
331     QString u = QUrl::fromPercentEncoding( url.url().toUtf8() );
332     return u.startsWith( m_mountPoint ) || u.startsWith( "file://" + m_mountPoint );
333 }
334 
335 Meta::TrackPtr
trackForUrl(const QUrl & url)336 UmsCollection::trackForUrl( const QUrl &url )
337 {
338     //not initialized yet.
339     if( m_mc.isNull() )
340         return Meta::TrackPtr();
341 
342     QString uid = QUrl::fromPercentEncoding( url.url().toUtf8() );
343     if( uid.startsWith("file://") )
344         uid = uid.remove( 0, 7 );
345     return m_mc->trackMap().value( uid, Meta::TrackPtr() );
346 }
347 
348 QueryMaker *
queryMaker()349 UmsCollection::queryMaker()
350 {
351     return new MemoryQueryMaker( m_mc.toWeakRef(), collectionId() );
352 }
353 
354 QString
uidUrlProtocol() const355 UmsCollection::uidUrlProtocol() const
356 {
357     return QStringLiteral( "file://" );
358 }
359 
360 QString
collectionId() const361 UmsCollection::collectionId() const
362 {
363     return m_collectionId;
364 }
365 
366 QString
prettyName() const367 UmsCollection::prettyName() const
368 {
369     QString actualName;
370     if( !m_collectionName.isEmpty() )
371         actualName = m_collectionName;
372     else if( !m_device.description().isEmpty() )
373         actualName = m_device.description();
374     else
375     {
376         actualName = m_device.vendor().simplified();
377         if( !actualName.isEmpty() )
378             actualName += ' ';
379         actualName += m_device.product().simplified();
380     }
381 
382     if( m_tracksParsed )
383         return actualName;
384     else
385         return i18nc( "Name of the USB Mass Storage collection that has not yet been "
386                       "activated. See also the 'Activate This Collection' action; %1 is "
387                       "actual collection name", "%1 (not activated)", actualName );
388 }
389 
390 QIcon
icon() const391 UmsCollection::icon() const
392 {
393     if( m_device.icon().isEmpty() )
394         return QIcon::fromTheme( "drive-removable-media-usb-pendrive" );
395     else
396         return QIcon::fromTheme( m_device.icon() );
397 }
398 
399 bool
hasCapacity() const400 UmsCollection::hasCapacity() const
401 {
402     if( m_device.isValid() && m_device.is<Solid::StorageAccess>() )
403         return m_device.as<Solid::StorageAccess>()->isAccessible();
404     return false;
405 }
406 
407 float
usedCapacity() const408 UmsCollection::usedCapacity() const
409 {
410     return KDiskFreeSpaceInfo::freeSpaceInfo( m_mountPoint ).used();
411 }
412 
413 float
totalCapacity() const414 UmsCollection::totalCapacity() const
415 {
416     return KDiskFreeSpaceInfo::freeSpaceInfo( m_mountPoint ).size();
417 }
418 
419 CollectionLocation *
location()420 UmsCollection::location()
421 {
422     return new UmsCollectionLocation( this );
423 }
424 
425 bool
isOrganizable() const426 UmsCollection::isOrganizable() const
427 {
428     return isWritable();
429 }
430 
431 bool
hasCapabilityInterface(Capabilities::Capability::Type type) const432 UmsCollection::hasCapabilityInterface( Capabilities::Capability::Type type ) const
433 {
434     switch( type )
435     {
436         case Capabilities::Capability::Actions:
437         case Capabilities::Capability::Transcode:
438             return true;
439         default:
440             return false;
441     }
442 }
443 
444 Capabilities::Capability *
createCapabilityInterface(Capabilities::Capability::Type type)445 UmsCollection::createCapabilityInterface( Capabilities::Capability::Type type )
446 {
447     switch( type )
448     {
449         case Capabilities::Capability::Actions:
450         {
451             QList<QAction *> actions;
452             if( m_tracksParsed )
453             {
454                 actions << m_configureAction;
455                 actions << m_ejectAction;
456             }
457             else
458             {
459                 actions << m_parseAction;
460             }
461             return new Capabilities::ActionsCapability( actions );
462         }
463         case Capabilities::Capability::Transcode:
464             return new UmsTranscodeCapability( m_mountPoint + QLatin1Char('/') + s_settingsFileName,
465                                                s_transcodingGroup );
466         default:
467             return nullptr;
468     }
469 }
470 
471 void
metadataChanged(const Meta::TrackPtr & track)472 UmsCollection::metadataChanged(const Meta::TrackPtr &track )
473 {
474     if( MemoryMeta::MapChanger( m_mc.data() ).trackChanged( track ) )
475         // big-enough change:
476         Q_EMIT startUpdateTimer();
477 }
478 
479 QUrl
organizedUrl(const Meta::TrackPtr & track,const QString & fileExtension) const480 UmsCollection::organizedUrl( const Meta::TrackPtr &track, const QString &fileExtension ) const
481 {
482     TrackOrganizer trackOrganizer( Meta::TrackList() << track );
483     //%folder% prefix required to get absolute url.
484     trackOrganizer.setFormatString( "%collectionroot%/" + m_musicFilenameScheme + ".%filetype%" );
485     trackOrganizer.setVfatSafe( m_vfatSafe );
486     trackOrganizer.setAsciiOnly( m_asciiOnly );
487     trackOrganizer.setFolderPrefix( m_musicUrl.path() );
488     trackOrganizer.setPostfixThe( m_postfixThe );
489     trackOrganizer.setReplaceSpaces( m_replaceSpaces );
490     trackOrganizer.setReplace( m_regexText, m_replaceText );
491     if( !fileExtension.isEmpty() )
492         trackOrganizer.setTargetFileExtension( fileExtension );
493 
494     return QUrl::fromLocalFile( trackOrganizer.getDestinations().value( track ) );
495 }
496 
497 void
slotDestroy()498 UmsCollection::slotDestroy()
499 {
500     //TODO: stop scanner if running
501     //unregister PlaylistProvider
502     //CollectionManager will call destructor.
503     Q_EMIT remove();
504 }
505 
506 void
slotEject()507 UmsCollection::slotEject()
508 {
509     slotDestroy();
510     Solid::StorageAccess *storageAccess = m_device.as<Solid::StorageAccess>();
511     storageAccess->teardown();
512 }
513 
514 void
slotTrackAdded(const QUrl & location)515 UmsCollection::slotTrackAdded( const QUrl &location )
516 {
517     Q_ASSERT( m_musicUrl.isParentOf( location ) || m_musicUrl.matches( location , QUrl::StripTrailingSlash) );
518     MetaFile::Track *fileTrack = new MetaFile::Track( location );
519     fileTrack->setCollection( this );
520     Meta::TrackPtr fileTrackPtr = Meta::TrackPtr( fileTrack );
521     Meta::TrackPtr proxyTrack = MemoryMeta::MapChanger( m_mc.data() ).addTrack( fileTrackPtr );
522     if( proxyTrack )
523     {
524         subscribeTo( fileTrackPtr );
525         Q_EMIT startUpdateTimer();
526     }
527     else
528         warning() << __PRETTY_FUNCTION__ << "Failed to add" << fileTrackPtr->playableUrl()
529                   << "to MemoryCollection. Perhaps already there?!?";
530 }
531 
532 void
slotTrackRemoved(const Meta::TrackPtr & track)533 UmsCollection::slotTrackRemoved( const Meta::TrackPtr &track )
534 {
535     Meta::TrackPtr removedTrack = MemoryMeta::MapChanger( m_mc.data() ).removeTrack( track );
536     if( removedTrack )
537     {
538         unsubscribeFrom( removedTrack );
539         // we only added MetaFile::Tracks, following static cast is safe
540         static_cast<MetaFile::Track*>( removedTrack.data() )->setCollection( 0 );
541         Q_EMIT startUpdateTimer();
542     }
543     else
544         warning() << __PRETTY_FUNCTION__ << "Failed to remove" << track->playableUrl()
545                   << "from MemoryCollection. Perhaps it was never there?";
546 }
547 
548 void
collectionUpdated()549 UmsCollection::collectionUpdated()
550 {
551     m_lastUpdated = QDateTime::currentMSecsSinceEpoch();
552     Q_EMIT updated();
553 }
554 
555 void
slotParseTracks()556 UmsCollection::slotParseTracks()
557 {
558     if( !m_scanManager )
559     {
560         m_scanManager = new GenericScanManager( this );
561         connect( m_scanManager, &GenericScanManager::directoryScanned,
562                  this, &UmsCollection::slotDirectoryScanned );
563     }
564 
565     m_tracksParsed = true;
566     m_scanManager->requestScan( QList<QUrl>() << m_musicUrl, GenericScanManager::FullScan );
567 }
568 
569 void
slotParseActionTriggered()570 UmsCollection::slotParseActionTriggered()
571 {
572     if( m_mc->trackMap().isEmpty() )
573         QTimer::singleShot( 0, this, &UmsCollection::slotParseTracks );
574 }
575 
576 void
slotConfigure()577 UmsCollection::slotConfigure()
578 {
579     QDialog umsSettingsDialog;
580     QWidget *settingsWidget = new QWidget( &umsSettingsDialog );
581     QScopedPointer<Capabilities::TranscodeCapability> tc( create<Capabilities::TranscodeCapability>() );
582 
583     Ui::UmsConfiguration *settings = new Ui::UmsConfiguration();
584     settings->setupUi( settingsWidget );
585 
586     settings->m_autoConnect->setChecked( m_autoConnect );
587 
588     settings->m_musicFolder->setMode( KFile::Directory );
589     settings->m_musicCheckBox->setChecked( !m_musicUrl.isEmpty() );
590     settings->m_musicWidget->setEnabled( settings->m_musicCheckBox->isChecked() );
591     settings->m_musicFolder->setUrl( m_musicUrl.isEmpty() ? QUrl::fromLocalFile( m_mountPoint ) : m_musicUrl );
592     settings->m_transcodeConfig->fillInChoices( tc->savedConfiguration() );
593 
594     settings->m_podcastFolder->setMode( KFile::Directory );
595     settings->m_podcastCheckBox->setChecked( !m_podcastUrl.isEmpty() );
596     settings->m_podcastWidget->setEnabled( settings->m_podcastCheckBox->isChecked() );
597     settings->m_podcastFolder->setUrl( m_podcastUrl.isEmpty() ? QUrl::fromLocalFile( m_mountPoint )
598                                          : m_podcastUrl );
599 
600     settings->m_collectionName->setText( prettyName() );
601 
602     OrganizeCollectionWidget *layoutWidget = new OrganizeCollectionWidget;
603     //TODO: save the setting that are normally written in onAccept()
604 //    connect( this, SIGNAL(accepted()), &layoutWidget, SLOT(onAccept()) );
605     QVBoxLayout *layout = new QVBoxLayout;
606     layout->addWidget( layoutWidget );
607     settings->m_filenameSchemeBox->setLayout( layout );
608     //hide the unuse preset selector.
609     //TODO: change the presets to concurrent presets for regular albums v.s. compilations
610     // layoutWidget.setformatPresetVisible( false );
611     layoutWidget->setScheme( m_musicFilenameScheme );
612 
613     OrganizeCollectionOptionWidget *optionsWidget = new OrganizeCollectionOptionWidget;
614     optionsWidget->setVfatCompatible( m_vfatSafe );
615     optionsWidget->setAsciiOnly( m_asciiOnly );
616     optionsWidget->setPostfixThe( m_postfixThe );
617     optionsWidget->setReplaceSpaces( m_replaceSpaces );
618     optionsWidget->setRegexpText( m_regexText );
619     optionsWidget->setReplaceText( m_replaceText );
620 
621     layout->addWidget( optionsWidget );
622 
623     umsSettingsDialog.setLayout( new QVBoxLayout );
624     QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel );
625     connect( buttonBox, &QDialogButtonBox::accepted, &umsSettingsDialog, &QDialog::accept );
626     connect( buttonBox, &QDialogButtonBox::rejected, &umsSettingsDialog, &QDialog::reject );
627     umsSettingsDialog.layout()->addWidget( settingsWidget );
628     umsSettingsDialog.layout()->addWidget( buttonBox );
629     umsSettingsDialog.setWindowTitle( i18n( "Configure USB Mass Storage Device" ) );
630 
631     if( umsSettingsDialog.exec() == QDialog::Accepted )
632     {
633         debug() << "accepted";
634 
635         if( settings->m_musicCheckBox->isChecked() )
636         {
637             if( settings->m_musicFolder->url() != m_musicUrl )
638             {
639                 debug() << "music location changed from " << m_musicUrl.toLocalFile() << " to ";
640                 debug() << settings->m_musicFolder->url().toLocalFile();
641                 m_musicUrl = settings->m_musicFolder->url();
642                 //TODO: reparse music
643             }
644             QString scheme = layoutWidget->getParsableScheme().simplified();
645             //protect against empty string.
646             if( !scheme.isEmpty() )
647                 m_musicFilenameScheme = scheme;
648         }
649         else
650         {
651             debug() << "music support is disabled";
652             m_musicUrl = QUrl();
653             //TODO: remove all tracks from the MemoryCollection.
654         }
655 
656         m_asciiOnly = optionsWidget->asciiOnly();
657         m_postfixThe = optionsWidget->postfixThe();
658         m_replaceSpaces = optionsWidget->replaceSpaces();
659         m_regexText = optionsWidget->regexpText();
660         m_replaceText = optionsWidget->replaceText();
661         m_collectionName = settings->m_collectionName->text();
662 
663         if( settings->m_podcastCheckBox->isChecked() )
664         {
665             if( settings->m_podcastFolder->url() != m_podcastUrl )
666             {
667                 debug() << "podcast location changed from " << m_podcastUrl << " to ";
668                 debug() << settings->m_podcastFolder->url().url();
669                 m_podcastUrl = QUrl(settings->m_podcastFolder->url());
670                 //TODO: reparse podcasts
671             }
672         }
673         else
674         {
675             debug() << "podcast support is disabled";
676             m_podcastUrl = QUrl();
677             //TODO: remove the PodcastProvider
678         }
679 
680         m_autoConnect = settings->m_autoConnect->isChecked();
681         if( !m_musicUrl.isEmpty() && m_autoConnect )
682             QTimer::singleShot( 0, this, &UmsCollection::slotParseTracks );
683 
684         // write the data to the on-disk file
685         KConfig config( m_mountPoint + QLatin1Char('/') + s_settingsFileName, KConfig::SimpleConfig );
686         KConfigGroup entries = config.group( QString() ); // default group
687         if( !m_musicUrl.isEmpty() )
688             entries.writePathEntry( s_musicFolderKey, QDir( m_mountPoint ).relativeFilePath( m_musicUrl.toLocalFile() ));
689         else
690             entries.deleteEntry( s_musicFolderKey );
691         entries.writeEntry( s_musicFilenameSchemeKey, m_musicFilenameScheme );
692         entries.writeEntry( s_vfatSafeKey, m_vfatSafe );
693         entries.writeEntry( s_asciiOnlyKey, m_asciiOnly );
694         entries.writeEntry( s_postfixTheKey, m_postfixThe );
695         entries.writeEntry( s_replaceSpacesKey, m_replaceSpaces );
696         entries.writeEntry( s_regexTextKey, m_regexText );
697         entries.writeEntry( s_replaceTextKey, m_replaceText );
698         if( !m_podcastUrl.isEmpty() )
699             entries.writePathEntry( s_podcastFolderKey, QDir( m_mountPoint ).relativeFilePath( m_podcastUrl.toLocalFile() ));
700         else
701             entries.deleteEntry( s_podcastFolderKey );
702         entries.writeEntry( s_autoConnectKey, m_autoConnect );
703         entries.writeEntry( s_collectionName, m_collectionName );
704         config.sync();
705 
706         tc->setSavedConfiguration( settings->m_transcodeConfig->currentChoice() );
707     }
708 
709     delete settings;
710 }
711 
712 void
slotDirectoryScanned(QSharedPointer<CollectionScanner::Directory> dir)713 UmsCollection::slotDirectoryScanned( QSharedPointer<CollectionScanner::Directory> dir )
714 {
715     debug() << "directory scanned: " << dir->path();
716     if( dir->tracks().isEmpty() )
717     {
718         debug() << "does not have tracks";
719         return;
720     }
721 
722     foreach( const CollectionScanner::Track *scannerTrack, dir->tracks() )
723     {
724         //TODO: use proxy tracks so no real file read is required
725         // following method calls startUpdateTimer(), no need to Q_EMIT updated()
726         slotTrackAdded( QUrl::fromLocalFile(scannerTrack->path()) );
727     }
728 
729     //TODO: read playlists
730 }
731 
732 void
slotStartUpdateTimer()733 UmsCollection::slotStartUpdateTimer()
734 {
735     // there are no concurrency problems, this method can only be called from the main
736     // thread and that's where the timer fires
737     if( m_updateTimer.isActive() )
738         return; // already running, nothing to do
739 
740     // number of milliseconds to next desired update, may be negative
741     int timeout = m_lastUpdated + 1000 - QDateTime::currentMSecsSinceEpoch();
742     // give at least 50 msecs to catch multi-tracks edits nicely on the first frame
743     m_updateTimer.start( qBound( 50, timeout, 1000 ) );
744 }
745