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