1 #include "library/trackcollection.h"
2
3 #include <QApplication>
4
5 #include "library/basetrackcache.h"
6 #include "moc_trackcollection.cpp"
7 #include "track/globaltrackcache.h"
8 #include "util/assert.h"
9 #include "util/db/sqltransaction.h"
10 #include "util/dnd.h"
11 #include "util/logger.h"
12
13 namespace {
14
15 mixxx::Logger kLogger("TrackCollection");
16
17 } // anonymous namespace
18
TrackCollection(QObject * parent,const UserSettingsPointer & pConfig)19 TrackCollection::TrackCollection(
20 QObject* parent,
21 const UserSettingsPointer& pConfig)
22 : QObject(parent),
23 m_analysisDao(pConfig),
24 m_trackDao(m_cueDao, m_playlistDao,
25 m_analysisDao, m_libraryHashDao, pConfig) {
26 // Forward signals from TrackDAO
27 connect(&m_trackDao,
28 &TrackDAO::trackClean,
29 this,
30 &TrackCollection::trackClean,
31 /*signal-to-signal*/ Qt::DirectConnection);
32 connect(&m_trackDao,
33 &TrackDAO::trackDirty,
34 this,
35 &TrackCollection::trackDirty,
36 /*signal-to-signal*/ Qt::DirectConnection);
37 connect(&m_trackDao,
38 &TrackDAO::tracksAdded,
39 this,
40 &TrackCollection::tracksAdded,
41 /*signal-to-signal*/ Qt::DirectConnection);
42 connect(&m_trackDao,
43 &TrackDAO::tracksChanged,
44 this,
45 &TrackCollection::tracksChanged,
46 /*signal-to-signal*/ Qt::DirectConnection);
47 connect(&m_trackDao,
48 &TrackDAO::tracksRemoved,
49 this,
50 &TrackCollection::tracksRemoved,
51 /*signal-to-signal*/ Qt::DirectConnection);
52 connect(&m_trackDao,
53 &TrackDAO::forceModelUpdate,
54 this,
55 &TrackCollection::multipleTracksChanged,
56 /*signal-to-signal*/ Qt::DirectConnection);
57 }
58
~TrackCollection()59 TrackCollection::~TrackCollection() {
60 if (kLogger.debugEnabled()) {
61 kLogger.debug() << "~TrackCollection()";
62 }
63 // The database should have been detached earlier
64 DEBUG_ASSERT(!m_database.isOpen());
65 }
66
repairDatabase(const QSqlDatabase & database)67 void TrackCollection::repairDatabase(const QSqlDatabase& database) {
68 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
69
70 kLogger.info() << "Repairing database";
71 m_crates.repairDatabase(database);
72 }
73
connectDatabase(const QSqlDatabase & database)74 void TrackCollection::connectDatabase(const QSqlDatabase& database) {
75 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
76
77 kLogger.info() << "Connecting database";
78 m_database = database;
79 m_trackDao.initialize(database);
80 m_playlistDao.initialize(database);
81 m_cueDao.initialize(database);
82 m_directoryDao.initialize(database);
83 m_analysisDao.initialize(database);
84 m_libraryHashDao.initialize(database);
85 m_crates.connectDatabase(database);
86 }
87
disconnectDatabase()88 void TrackCollection::disconnectDatabase() {
89 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
90
91 kLogger.info() << "Disconnecting database";
92 m_database = QSqlDatabase();
93 m_trackDao.finish();
94 m_crates.disconnectDatabase();
95 }
96
connectTrackSource(QSharedPointer<BaseTrackCache> pTrackSource)97 void TrackCollection::connectTrackSource(QSharedPointer<BaseTrackCache> pTrackSource) {
98 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
99
100 VERIFY_OR_DEBUG_ASSERT(m_pTrackSource.isNull()) {
101 kLogger.warning() << "Track source has already been connected";
102 return;
103 }
104 kLogger.info() << "Connecting track source";
105 m_pTrackSource = pTrackSource;
106 connect(this,
107 &TrackCollection::scanTrackAdded,
108 m_pTrackSource.data(),
109 &BaseTrackCache::slotScanTrackAdded);
110 connect(&m_trackDao,
111 &TrackDAO::trackDirty,
112 m_pTrackSource.data(),
113 &BaseTrackCache::slotTrackDirty);
114 connect(&m_trackDao,
115 &TrackDAO::trackClean,
116 m_pTrackSource.data(),
117 &BaseTrackCache::slotTrackClean);
118 connect(&m_trackDao,
119 &TrackDAO::tracksAdded,
120 m_pTrackSource.data(),
121 &BaseTrackCache::slotTracksAddedOrChanged);
122 connect(&m_trackDao,
123 &TrackDAO::tracksChanged,
124 m_pTrackSource.data(),
125 &BaseTrackCache::slotTracksAddedOrChanged);
126 connect(&m_trackDao,
127 &TrackDAO::tracksRemoved,
128 m_pTrackSource.data(),
129 &BaseTrackCache::slotTracksRemoved);
130 }
131
disconnectTrackSource()132 QWeakPointer<BaseTrackCache> TrackCollection::disconnectTrackSource() {
133 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
134
135 auto pWeakPtr = m_pTrackSource.toWeakRef();
136 if (m_pTrackSource) {
137 kLogger.info() << "Disconnecting track source";
138 m_trackDao.disconnect(m_pTrackSource.data());
139 m_pTrackSource.reset();
140 }
141 return pWeakPtr;
142 }
143
addDirectory(const QString & dir)144 bool TrackCollection::addDirectory(const QString& dir) {
145 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
146
147 SqlTransaction transaction(m_database);
148 switch (m_directoryDao.addDirectory(dir)) {
149 case SQL_ERROR:
150 return false;
151 case ALREADY_WATCHING:
152 return true;
153 case ALL_FINE:
154 transaction.commit();
155 return true;
156 default:
157 DEBUG_ASSERT("unreachable");
158 }
159 return false;
160 }
161
removeDirectory(const QString & dir)162 bool TrackCollection::removeDirectory(const QString& dir) {
163 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
164
165 SqlTransaction transaction(m_database);
166 switch (m_directoryDao.removeDirectory(dir)) {
167 case SQL_ERROR:
168 return false;
169 case ALL_FINE:
170 transaction.commit();
171 return true;
172 default:
173 DEBUG_ASSERT("unreachable");
174 }
175 return false;
176 }
177
relocateDirectory(const QString & oldDir,const QString & newDir)178 void TrackCollection::relocateDirectory(const QString& oldDir, const QString& newDir) {
179 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
180
181 // We only call this method if the user has picked a relocated directory via
182 // a file dialog. This means the system sandboxer (if we are sandboxed) has
183 // granted us permission to this folder. Create a security bookmark while we
184 // have permission so that we can access the folder on future runs. We need
185 // to canonicalize the path so we first wrap the directory string with a
186 // QDir.
187 Sandbox::createSecurityToken(QDir(newDir));
188
189 SqlTransaction transaction(m_database);
190 QList<RelocatedTrack> relocatedTracks =
191 m_directoryDao.relocateDirectory(oldDir, newDir);
192 transaction.commit();
193
194 if (relocatedTracks.isEmpty()) {
195 // No tracks moved
196 return;
197 }
198
199 // Inform the TrackDAO about the changes
200 m_trackDao.slotDatabaseTracksRelocated(std::move(relocatedTracks));
201
202 GlobalTrackCacheLocker().relocateCachedTracks(&m_trackDao);
203 }
204
resolveTrackIds(const QList<TrackFile> & trackFiles,TrackDAO::ResolveTrackIdFlags flags)205 QList<TrackId> TrackCollection::resolveTrackIds(
206 const QList<TrackFile>& trackFiles,
207 TrackDAO::ResolveTrackIdFlags flags) {
208 QList<TrackId> trackIds = m_trackDao.resolveTrackIds(trackFiles, flags);
209 if (flags & TrackDAO::ResolveTrackIdFlag::UnhideHidden) {
210 unhideTracks(trackIds);
211 }
212 return trackIds;
213 }
214
resolveTrackIdsFromUrls(const QList<QUrl> & urls,bool addMissing)215 QList<TrackId> TrackCollection::resolveTrackIdsFromUrls(
216 const QList<QUrl>& urls, bool addMissing) {
217 QList<TrackFile> files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true);
218 if (files.isEmpty()) {
219 return QList<TrackId>();
220 }
221
222 TrackDAO::ResolveTrackIdFlags flags =
223 TrackDAO::ResolveTrackIdFlag::UnhideHidden;
224 if (addMissing) {
225 flags |= TrackDAO::ResolveTrackIdFlag::AddMissing;
226 }
227 return resolveTrackIds(files, flags);
228 }
229
resolveTrackIdsFromLocations(const QList<QString> & locations)230 QList<TrackId> TrackCollection::resolveTrackIdsFromLocations(
231 const QList<QString>& locations) {
232 QList<TrackFile> trackFiles;
233 trackFiles.reserve(locations.size());
234 for (const QString& location : locations) {
235 trackFiles.append(TrackFile(location));
236 }
237 return resolveTrackIds(trackFiles,
238 TrackDAO::ResolveTrackIdFlag::UnhideHidden
239 | TrackDAO::ResolveTrackIdFlag::AddMissing);
240 }
241
hideTracks(const QList<TrackId> & trackIds)242 bool TrackCollection::hideTracks(const QList<TrackId>& trackIds) {
243 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
244
245 // Warn if tracks have a playlist membership
246 QSet<int> allPlaylistIds;
247 for (const auto& trackId: trackIds) {
248 QSet<int> playlistIds;
249 m_playlistDao.getPlaylistsTrackIsIn(trackId, &playlistIds);
250 for (const auto& playlistId : qAsConst(playlistIds)) {
251 if (m_playlistDao.getHiddenType(playlistId) != PlaylistDAO::PLHT_SET_LOG) {
252 allPlaylistIds.insert(playlistId);
253 }
254 }
255 }
256
257 if (!allPlaylistIds.isEmpty()) {
258 QStringList playlistNames;
259 playlistNames.reserve(allPlaylistIds.count());
260 for (const auto& playlistId: allPlaylistIds) {
261 playlistNames.append(m_playlistDao.getPlaylistName(playlistId));
262 }
263
264 QString playlistNamesSection =
265 "\n\n\"" %
266 playlistNames.join("\"\n\"") %
267 "\"\n\n";
268
269 if (QMessageBox::question(
270 nullptr,
271 tr("Hiding tracks"),
272 tr("The selected tracks are in the following playlists:"
273 "%1"
274 "Hiding them will remove them from these playlists. Continue?")
275 .arg(playlistNamesSection),
276 QMessageBox::Ok | QMessageBox::Cancel) != QMessageBox::Ok) {
277 return false;
278 }
279 }
280
281 // Transactional
282 SqlTransaction transaction(m_database);
283 VERIFY_OR_DEBUG_ASSERT(transaction) {
284 return false;
285 }
286 VERIFY_OR_DEBUG_ASSERT(m_trackDao.hideTracks(trackIds)) {
287 return false;
288 }
289 VERIFY_OR_DEBUG_ASSERT(transaction.commit()) {
290 return false;
291 }
292
293 m_playlistDao.removeTracksFromPlaylists(trackIds);
294
295 // Post-processing
296 // TODO(XXX): Move signals from TrackDAO to TrackCollection
297 m_trackDao.afterHidingTracks(trackIds);
298 QSet<CrateId> modifiedCrateSummaries(
299 m_crates.collectCrateIdsOfTracks(trackIds));
300
301 // Emit signal(s)
302 // TODO(XXX): Emit signals here instead of from DAOs
303 emit crateSummaryChanged(modifiedCrateSummaries);
304
305 return true;
306 }
307
hideAllTracks(const QDir & rootDir)308 void TrackCollection::hideAllTracks(const QDir& rootDir) {
309 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
310
311 m_trackDao.hideAllTracks(rootDir);
312 }
313
unhideTracks(const QList<TrackId> & trackIds)314 bool TrackCollection::unhideTracks(const QList<TrackId>& trackIds) {
315 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
316
317 VERIFY_OR_DEBUG_ASSERT(m_trackDao.unhideTracks(trackIds)) {
318 return false;
319 }
320
321 // Post-processing
322 // TODO(XXX): Move signals from TrackDAO to TrackCollection
323 // to update BaseTrackCache
324 m_trackDao.afterUnhidingTracks(trackIds);
325
326 // Emit signal(s)
327 // TODO(XXX): Emit signals here instead of from DAOs
328 // To update labels of CrateFeature, because unhiding might make a
329 // crate track visible again.
330 QSet<CrateId> modifiedCrateSummaries =
331 m_crates.collectCrateIdsOfTracks(trackIds);
332 emit crateSummaryChanged(modifiedCrateSummaries);
333
334 return true;
335 }
336
purgeTracks(const QList<TrackId> & trackIds)337 bool TrackCollection::purgeTracks(
338 const QList<TrackId>& trackIds) {
339 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
340
341 // Transactional
342 SqlTransaction transaction(m_database);
343 VERIFY_OR_DEBUG_ASSERT(transaction) {
344 return false;
345 }
346 VERIFY_OR_DEBUG_ASSERT(m_trackDao.onPurgingTracks(trackIds)) {
347 return false;
348 }
349 // Collect crates of tracks that will be purged before actually purging
350 // them within the same transactions. Those tracks will be removed from
351 // all crates on purging.
352 QSet<CrateId> modifiedCrateSummaries(
353 m_crates.collectCrateIdsOfTracks(trackIds));
354 VERIFY_OR_DEBUG_ASSERT(m_crates.onPurgingTracks(trackIds)) {
355 return false;
356 }
357 VERIFY_OR_DEBUG_ASSERT(transaction.commit()) {
358 return false;
359 }
360 // TODO(XXX): Move reversible actions inside transaction
361 m_cueDao.deleteCuesForTracks(trackIds);
362 m_playlistDao.removeTracksFromPlaylists(trackIds);
363 m_analysisDao.deleteAnalyses(trackIds);
364
365 // Post-processing
366 // TODO(XXX): Move signals from TrackDAO to TrackCollection
367 m_trackDao.afterPurgingTracks(trackIds);
368
369 // Emit signal(s)
370 // TODO(XXX): Emit signals here instead of from DAOs
371 emit crateSummaryChanged(modifiedCrateSummaries);
372
373 return true;
374 }
375
purgeAllTracks(const QDir & rootDir)376 bool TrackCollection::purgeAllTracks(
377 const QDir& rootDir) {
378 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
379
380 QList<TrackRef> trackRefs = m_trackDao.getAllTrackRefs(rootDir);
381 QList<TrackId> trackIds;
382 trackIds.reserve(trackRefs.size());
383 for (const auto& trackRef : trackRefs) {
384 DEBUG_ASSERT(trackRef.hasId());
385 trackIds.append(trackRef.getId());
386 }
387 return purgeTracks(trackIds);
388 }
389
insertCrate(const Crate & crate,CrateId * pCrateId)390 bool TrackCollection::insertCrate(
391 const Crate& crate,
392 CrateId* pCrateId) {
393 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
394
395 // Transactional
396 SqlTransaction transaction(m_database);
397 VERIFY_OR_DEBUG_ASSERT(transaction) {
398 return false;
399 }
400 CrateId crateId;
401 VERIFY_OR_DEBUG_ASSERT(m_crates.onInsertingCrate(crate, &crateId)) {
402 return false;
403 }
404 DEBUG_ASSERT(crateId.isValid());
405 VERIFY_OR_DEBUG_ASSERT(transaction.commit()) {
406 return false;
407 }
408
409 // Emit signals
410 emit crateInserted(crateId);
411
412 if (pCrateId != nullptr) {
413 *pCrateId = crateId;
414 }
415 return true;
416 }
417
updateCrate(const Crate & crate)418 bool TrackCollection::updateCrate(
419 const Crate& crate) {
420 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
421
422 // Transactional
423 SqlTransaction transaction(m_database);
424 VERIFY_OR_DEBUG_ASSERT(transaction) {
425 return false;
426 }
427 VERIFY_OR_DEBUG_ASSERT(m_crates.onUpdatingCrate(crate)) {
428 return false;
429 }
430 VERIFY_OR_DEBUG_ASSERT(transaction.commit()) {
431 return false;
432 }
433
434 // Emit signals
435 emit crateUpdated(crate.getId());
436
437 return true;
438 }
439
deleteCrate(CrateId crateId)440 bool TrackCollection::deleteCrate(
441 CrateId crateId) {
442 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
443
444 // Transactional
445 SqlTransaction transaction(m_database);
446 VERIFY_OR_DEBUG_ASSERT(transaction) {
447 return false;
448 }
449 VERIFY_OR_DEBUG_ASSERT(m_crates.onDeletingCrate(crateId)) {
450 return false;
451 }
452 VERIFY_OR_DEBUG_ASSERT(transaction.commit()) {
453 return false;
454 }
455
456 // Emit signals
457 emit crateDeleted(crateId);
458
459 return true;
460 }
461
addCrateTracks(CrateId crateId,const QList<TrackId> & trackIds)462 bool TrackCollection::addCrateTracks(
463 CrateId crateId,
464 const QList<TrackId>& trackIds) {
465 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
466
467 // Transactional
468 SqlTransaction transaction(m_database);
469 VERIFY_OR_DEBUG_ASSERT(transaction) {
470 return false;
471 }
472 VERIFY_OR_DEBUG_ASSERT(m_crates.onAddingCrateTracks(crateId, trackIds)) {
473 return false;
474 }
475 VERIFY_OR_DEBUG_ASSERT(transaction.commit()) {
476 return false;
477 }
478
479 // Emit signals
480 emit crateTracksChanged(crateId, trackIds, QList<TrackId>());
481
482 return true;
483 }
484
removeCrateTracks(CrateId crateId,const QList<TrackId> & trackIds)485 bool TrackCollection::removeCrateTracks(
486 CrateId crateId,
487 const QList<TrackId>& trackIds) {
488 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
489
490 // Transactional
491 SqlTransaction transaction(m_database);
492 VERIFY_OR_DEBUG_ASSERT(transaction) {
493 return false;
494 }
495 VERIFY_OR_DEBUG_ASSERT(m_crates.onRemovingCrateTracks(crateId, trackIds)) {
496 return false;
497 }
498 VERIFY_OR_DEBUG_ASSERT(transaction.commit()) {
499 return false;
500 }
501
502 // Emit signals
503 emit crateTracksChanged(crateId, QList<TrackId>(), trackIds);
504
505 return true;
506 }
507
updateAutoDjCrate(CrateId crateId,bool isAutoDjSource)508 bool TrackCollection::updateAutoDjCrate(
509 CrateId crateId,
510 bool isAutoDjSource) {
511 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
512
513 Crate crate;
514 VERIFY_OR_DEBUG_ASSERT(crates().readCrateById(crateId, &crate)) {
515 return false; // inexistent or failure
516 }
517 if (crate.isAutoDjSource() == isAutoDjSource) {
518 return false; // nothing to do
519 }
520 crate.setAutoDjSource(isAutoDjSource);
521 return updateCrate(crate);
522 }
523
saveTrack(Track * pTrack)524 void TrackCollection::saveTrack(Track* pTrack) {
525 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
526
527 m_trackDao.saveTrack(pTrack);
528 }
529
getTrackById(TrackId trackId) const530 TrackPointer TrackCollection::getTrackById(
531 TrackId trackId) const {
532 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
533
534 return m_trackDao.getTrackById(trackId);
535 }
536
getTrackByRef(const TrackRef & trackRef) const537 TrackPointer TrackCollection::getTrackByRef(
538 const TrackRef& trackRef) const {
539 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
540
541 return m_trackDao.getTrackByRef(trackRef);
542 }
543
getTrackIdByRef(const TrackRef & trackRef) const544 TrackId TrackCollection::getTrackIdByRef(
545 const TrackRef& trackRef) const {
546 return m_trackDao.getTrackIdByRef(trackRef);
547 }
548
getOrAddTrack(const TrackRef & trackRef,bool * pAlreadyInLibrary)549 TrackPointer TrackCollection::getOrAddTrack(
550 const TrackRef& trackRef,
551 bool* pAlreadyInLibrary) {
552 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
553
554 return m_trackDao.getOrAddTrack(trackRef, pAlreadyInLibrary);
555 }
556
addTrack(const TrackPointer & pTrack,bool unremove)557 TrackId TrackCollection::addTrack(
558 const TrackPointer& pTrack,
559 bool unremove) {
560 DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
561
562 m_trackDao.addTracksPrepare();
563 const auto trackId = m_trackDao.addTracksAddTrack(pTrack, unremove);
564 m_trackDao.addTracksFinish(!trackId.isValid());
565 return trackId;
566 }
567