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