Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions src/library/dao/trackdao.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
#include "library/dao/trackdao.h"

#include <qhashfunctions.h>

#include <QChar>
#include <QDir>
#include <QFileInfo>
#include <QThread>
#include <QtDebug>

#include "track/trackref.h"

#ifdef __SQLITE3__
#include <sqlite3.h>
#endif // __SQLITE3__
Expand Down Expand Up @@ -1778,6 +1782,119 @@ bool TrackDAO::updateTrack(const Track& track) const {
return true;
}

// Relocate the file linked to the track
std::optional<RelocatedTrack> TrackDAO::relocateTrack(
const TrackId trackId, const mixxx::FileInfo& newLocation) {
DEBUG_ASSERT(trackId.isValid());

const QString oldLocation = getTrackLocation(trackId);

kLogger.debug() << "Relocating track" << trackId
<< "to" << newLocation
<< "from" << oldLocation;

SqlTransaction transaction(m_database);

// Check for a duplicate track location. This happens if the new location
// is inside mixxx library folder because it is automatically added to
// library. If there is no duplicate, the track is outside mixxx library folder.
DbId duplicateLocationId;
TrackId duplicateLocationTrackId;
FwdSqlQuery queryDuplicateLocation(m_database,
"SELECT track_locations.id, library.id "
"FROM track_locations "
"LEFT JOIN library ON library.location = track_locations.id "
"WHERE track_locations.location = :location "
"AND (library.id != :trackId OR library.id IS NULL)");
queryDuplicateLocation.bindValue(":location", newLocation.location());
queryDuplicateLocation.bindValue(":trackId", trackId.toVariant());
if (!queryDuplicateLocation.execPrepared()) {
return std::nullopt;
}
if (queryDuplicateLocation.next()) {
duplicateLocationId = DbId(queryDuplicateLocation.fieldValue(0));
duplicateLocationTrackId = TrackId(queryDuplicateLocation.fieldValue(1));
}
if (duplicateLocationId.isValid()) {
kLogger.debug() << "New track location already exist in db (location id: "
<< duplicateLocationId
<< "). Deleting duplicate track and linking new location to current track.";

// Fetch current track oprhaned location id.
DbId orphanedLocationId;
FwdSqlQuery queryOprhanedLocationId(m_database,
"SELECT location FROM library WHERE id = :trackId");
queryOprhanedLocationId.bindValue(":trackId", trackId.toVariant());
if (!queryOprhanedLocationId.execPrepared()) {
return std::nullopt;
}
if (queryOprhanedLocationId.next()) {
orphanedLocationId = DbId(queryOprhanedLocationId.fieldValue(0));
}

// Delete duplicate track.
FwdSqlQuery queryDeleteDuplicate(m_database,
"DELETE FROM library WHERE id = :newTrackId");
queryDeleteDuplicate.bindValue(":newTrackId",
duplicateLocationTrackId.toVariant());
if (!queryDeleteDuplicate.execPrepared()) {
return std::nullopt;
}

// Update current track location to new location (duplicate location database entry).
FwdSqlQuery queryUpdateLocation(m_database,
"UPDATE library SET location = :loc WHERE id = :trackId");
queryUpdateLocation.bindValue(":loc", duplicateLocationId.toVariant());
queryUpdateLocation.bindValue(":trackId", trackId.toVariant());
if (!queryUpdateLocation.execPrepared()) {
return std::nullopt;
}

// Delete orphaned location.
FwdSqlQuery queryDeleteOrphanedLocation(m_database,
"DELETE FROM track_locations WHERE id = :id");
queryDeleteOrphanedLocation.bindValue(":id",
orphanedLocationId.toVariant());
queryDeleteOrphanedLocation.execPrepared();
} else {
// Directly update orphaned location with new data since
// there is no duplicate.
FwdSqlQuery queryUpdateNoDuplicate(m_database,
"UPDATE track_locations SET "
"location = :location,"
"directory = :directory,"
"filename = :filename,"
"filesize = :filesize,"
"fs_deleted = 0,"
"needs_verification = 0 "
"WHERE id=(SELECT location FROM library WHERE id =:trackId)");
queryUpdateNoDuplicate.bindValue(":location", newLocation.location());
queryUpdateNoDuplicate.bindValue(":directory", newLocation.locationPath());
queryUpdateNoDuplicate.bindValue(":filename", newLocation.fileName());
queryUpdateNoDuplicate.bindValue(":filesize",
QVariant::fromValue(newLocation.sizeInBytes()));
queryUpdateNoDuplicate.bindValue(":trackId", trackId.toVariant());

if (!queryUpdateNoDuplicate.execPrepared()) {
return std::nullopt;
}
if (queryUpdateNoDuplicate.numRowsAffected() == 0) {
kLogger.warning() << "relocateTrack had no effect: trackId " << trackId << "invalid.";
return std::nullopt;
}
}
transaction.commit();

const auto missingTrackRef = TrackRef::fromFilePath(oldLocation, trackId);
TrackRef duplicateTrackRef = TrackRef();
if (duplicateLocationId.isValid()) {
duplicateTrackRef = TrackRef::fromFileInfo(newLocation, duplicateLocationTrackId);
} else {
duplicateTrackRef = TrackRef::fromFileInfo(newLocation);
}
return RelocatedTrack(missingTrackRef, duplicateTrackRef);
}
Comment thread
louisld marked this conversation as resolved.

@ronso0 ronso0 Jul 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The similarly named variables in this function make it a bit hard to understand the flow.
Let's rename for example
newTrackLocationId -> existingTrackLocationId
queryNewLocation -> queryExistingTrackLocation

and also add some comments:

Which cases it supposed to cover? (library scan already added a track with new location vs. new location not in db yet)
etc.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The else case covers the case where the new file is outside of the library folder.


// Make sure that `directory` in in track_locations table is indeed a
// directory path. This works around / removes residues of a bug where tracks
// are falsely marked missing because `directory` == `location`.
Expand Down
2 changes: 2 additions & 0 deletions src/library/dao/trackdao.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC
void addTracksFinish(bool rollback = false);

bool updateTrack(const Track& track) const;
std::optional<RelocatedTrack> relocateTrack(
const TrackId trackId, const mixxx::FileInfo& newLocation);

void hideAllTracks(const QDir& rootDir) const;

Expand Down
25 changes: 19 additions & 6 deletions src/library/missing_hidden/dlgmissing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ DlgMissing::DlgMissing(

connect(btnPurge, &QPushButton::clicked, m_pTrackTableView, &WTrackTableView::slotPurge);
connect(btnSelect, &QPushButton::clicked, this, &DlgMissing::selectAll);
connect(btnRelocate,
&QPushButton::clicked,
this,
&DlgMissing::slotRelocateTrack);
connect(m_pTrackTableView->selectionModel(),
&QItemSelectionModel::selectionChanged,
this,
Expand Down Expand Up @@ -75,14 +79,23 @@ void DlgMissing::selectAll() {
m_pTrackTableView->selectAll();
}

void DlgMissing::activateButtons(bool enable) {
btnPurge->setEnabled(enable);
void DlgMissing::slotRelocateTrack() {
const QModelIndexList indices = m_pTrackTableView->selectionModel()->selectedRows();
if (indices.count() != 1) {
return;
}

m_pMissingTableModel->relocateTrack(indices.first());
}

void DlgMissing::activateButtons(int numRowsSelected) {
btnPurge->setEnabled(numRowsSelected >= 1);
btnRelocate->setEnabled(numRowsSelected == 1);
}

void DlgMissing::selectionChanged(const QItemSelection &selected,
const QItemSelection &deselected) {
Q_UNUSED(deselected);
activateButtons(!selected.indexes().isEmpty());
void DlgMissing::selectionChanged([[maybe_unused]] const QItemSelection& selected,
[[maybe_unused]] const QItemSelection& deselected) {
activateButtons(m_pTrackTableView->selectionModel()->selectedRows().count());
}

bool DlgMissing::hasFocus() const {
Expand Down
5 changes: 4 additions & 1 deletion src/library/missing_hidden/dlgmissing.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ class DlgMissing : public QWidget, public Ui::DlgMissing, public LibraryView {
void trackSelected(TrackPointer pTrack);

private:
void activateButtons(bool enable);
void activateButtons(int numRowsSelected);
WTrackTableView* m_pTrackTableView;
MissingTableModel* m_pMissingTableModel;

private slots:
void slotRelocateTrack();
};
16 changes: 16 additions & 0 deletions src/library/missing_hidden/dlgmissing.ui
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnRelocate">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="toolTip">
<string>Relink the selected track to its new file location.</string>
</property>
<property name="text">
<string>Relink</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
Expand Down
44 changes: 44 additions & 0 deletions src/library/missing_hidden/missingtablemodel.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
#include "library/missing_hidden/missingtablemodel.h"

#include <qfiledialog.h>
#include <qstandardpaths.h>

#include "library/dao/trackschema.h"
#include "library/trackcollection.h"
#include "library/trackcollectionmanager.h"
#include "moc_missingtablemodel.cpp"
#include "sources/soundsourceproxy.h"
#include "track/track.h"

namespace {

Expand Down Expand Up @@ -62,6 +67,45 @@ void MissingTableModel::purgeTracks(const QModelIndexList& indices) {
select(); //Repopulate the data model.
}

void MissingTableModel::relocateTrack(const QModelIndex& index) {
QString location;
QString title;
TrackId trackId;
{
TrackPointer pTrack = getTrack(index);
if (!pTrack) {
return;
}

location = QFileInfo(pTrack->getLocation()).absolutePath();
title = pTrack->getTitle();
trackId = pTrack->getId();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason the above code is in an extra scope?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the track does not update in the cache because there are still reference linked to it. This was an attempt to delete the local reference but maybe it's useless since references are somewhere else.

if (location.isEmpty() || !QDir(location).exists()) {
location = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
}

const QString newLocation = QFileDialog::getOpenFileName(
nullptr,
tr("Locate missing file: %1").arg(title),
location,
QString("Audio Files (%1)")
.arg(SoundSourceProxy::getSupportedFileNamePatterns().join(" ")));

if (newLocation.isEmpty()) {
return;
}

const mixxx::FileInfo fileInfo(newLocation);
if (!trackId.isValid()) {
return;
}
Comment on lines +100 to +102

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since every track in Missing should have a valid id, this can go up right after the line where we read the id.
And it can become

VERIFY_OR_DEBUG_ASSERT(trackId.isvalid()) {
    // optional qWarning()
    return;
}


if (m_pTrackCollectionManager->relocateTrack(trackId, fileInfo)) {
select(); // Repopulate the data model
}
}

bool MissingTableModel::isColumnInternal(int column) {
return column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ID) ||
column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED) ||
Expand Down
2 changes: 2 additions & 0 deletions src/library/missing_hidden/missingtablemodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ class MissingTableModel final : public BaseSqlTableModel {
Capabilities getCapabilities() const final;

QString modelKey(bool noSearch) const override;

void relocateTrack(const QModelIndex& index);
Comment thread
louisld marked this conversation as resolved.
};
7 changes: 7 additions & 0 deletions src/library/trackcollection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,13 @@ bool TrackCollection::purgeAllTracks(
return purgeTracks(trackIds);
}

std::optional<RelocatedTrack> TrackCollection::relocateTrack(const TrackId trackId,
const mixxx::FileInfo& newLocation) {
DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);

return m_trackDao.relocateTrack(trackId, newLocation);
}

bool TrackCollection::insertCrate(
const Crate& crate,
CrateId* pCrateId) {
Expand Down
3 changes: 3 additions & 0 deletions src/library/trackcollection.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ class TrackCollection : public QObject,
bool purgeTracks(const QList<TrackId>& trackIds);
bool purgeAllTracks(const QDir& rootDir);

std::optional<RelocatedTrack> relocateTrack(
const TrackId trackId, const mixxx::FileInfo& newLocation);

DirectoryDAO::AddResult addDirectory(const mixxx::FileInfo& rootDir);
DirectoryDAO::RemoveResult removeDirectory(const mixxx::FileInfo& rootDir);
DirectoryDAO::RelocateResult relocateDirectory(const QString& oldDir, const QString& newDir);
Expand Down
17 changes: 17 additions & 0 deletions src/library/trackcollectionmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,23 @@ void TrackCollectionManager::purgeAllTracks(const QDir& rootDir) const {
}
}

bool TrackCollectionManager::relocateTrack(const TrackId trackId,
const mixxx::FileInfo& newLocation) {
DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);

const std::optional<RelocatedTrack> oRelocatedTrack =
m_pInternalCollection->relocateTrack(trackId, newLocation);
if (!oRelocatedTrack) {
return false;
}

m_pInternalCollection->getTrackDAO().slotDatabaseTracksRelocated({*oRelocatedTrack});
if (!m_externalCollections.isEmpty()) {
afterTracksRelocated({*oRelocatedTrack});
}
return true;
}

TrackPointer TrackCollectionManager::getOrAddTrack(
const TrackRef& trackRef,
bool* pAlreadyInLibrary) const {
Expand Down
2 changes: 2 additions & 0 deletions src/library/trackcollectionmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ class TrackCollectionManager: public QObject,
void purgeTracks(const QList<TrackRef>& trackRefs) const;
void purgeAllTracks(const QDir& rootDir) const;

bool relocateTrack(const TrackId trackId, const mixxx::FileInfo& newLocation);

DirectoryDAO::AddResult addDirectory(const mixxx::FileInfo& newDir) const;
DirectoryDAO::RemoveResult removeDirectory(const mixxx::FileInfo& oldDir) const;
DirectoryDAO::RelocateResult relocateDirectory(
Expand Down