Skip to content

Commit cfb2bfc

Browse files
committed
DAO: Add clearFingerprintData and clearAllFingerprintData to TrackFingerprintDao
1 parent 69c3d35 commit cfb2bfc

2 files changed

Lines changed: 218 additions & 0 deletions

File tree

src/library/dao/trackfingerprintdao.cpp

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,3 +1371,209 @@ bool TrackFingerprintDao::reQueueJob(TrackId trackId) const {
13711371
}
13721372
return affected;
13731373
}
1374+
1375+
bool TrackFingerprintDao::clearFingerprintData(TrackId trackId) const {
1376+
if (sDebugTrackFingerprintDao) {
1377+
qDebug() << "TrackFingerprintDao -> [clearFingerprintData] -> entry"
1378+
<< "trackId:" << trackId;
1379+
}
1380+
1381+
if (!m_database.isOpen() || !trackId.isValid()) {
1382+
qDebug() << "TrackFingerprintDao -> [clearFingerprintData] -> "
1383+
"aborting: database not open or invalid trackId";
1384+
return false;
1385+
}
1386+
1387+
// Step 1 — delete the .chroma file outside the transaction.
1388+
// File deletion is not transactional; doing it first means that if the
1389+
// DB steps later fail, the missing file is detected on the next analysis
1390+
// run and the fingerprint is recomputed. The reverse order would leave
1391+
// a valid DB row pointing at a missing file, which is harder to detect.
1392+
deleteChromaFile(trackId);
1393+
1394+
// Steps 2–5 run inside a ScopedTransaction so the DB is never left in a
1395+
// half-cleared state. ScopedTransaction stores its own QSqlDatabase copy
1396+
// and calls transaction()/commit()/rollback() on that copy — consistent
1397+
// with how TrackDAO uses ScopedTransaction for its write operations.
1398+
ScopedTransaction transaction(m_database);
1399+
if (!transaction.active()) {
1400+
qDebug() << "TrackFingerprintDao -> [clearFingerprintData] -> "
1401+
"failed to start transaction for trackId:"
1402+
<< trackId;
1403+
return false;
1404+
}
1405+
1406+
// Step 2 — handle CMRT group membership.
1407+
// If this track was canonical in its group and other members exist,
1408+
// promote another member to canonical. If it was the sole member,
1409+
// delete the group row. Non-canonical tracks skip this step.
1410+
// TODO (XXX): Update this logic to select next canonical track
1411+
// is a canonical Track is deleted, instead of just promoting the
1412+
// first member found in the group.
1413+
{
1414+
auto pMetadata = getFingerprintMetadata(trackId);
1415+
if (pMetadata && pMetadata->isCanonical && pMetadata->cmrtGroupId >= 0) {
1416+
const int groupId = pMetadata->cmrtGroupId;
1417+
1418+
// Look for another member in this group.
1419+
QSqlQuery q(m_database);
1420+
q.prepare(QStringLiteral(
1421+
"SELECT track_id FROM %1 "
1422+
"WHERE group_id = :group_id AND track_id != :track_id "
1423+
"LIMIT 1")
1424+
.arg(kCmrtMembersTableName));
1425+
q.bindValue(QStringLiteral(":group_id"), groupId);
1426+
q.bindValue(QStringLiteral(":track_id"), trackId.toVariant());
1427+
1428+
if (q.exec() && q.next()) {
1429+
// Another member exists — promote it to canonical.
1430+
const TrackId newCanonical(q.value(0));
1431+
1432+
QSqlQuery updateGroup(m_database);
1433+
updateGroup.prepare(QStringLiteral(
1434+
"UPDATE %1 SET canonical_track_id = :new_canonical "
1435+
"WHERE group_id = :group_id")
1436+
.arg(kCmrtGroupsTableName));
1437+
updateGroup.bindValue(QStringLiteral(":new_canonical"),
1438+
newCanonical.toVariant());
1439+
updateGroup.bindValue(QStringLiteral(":group_id"), groupId);
1440+
if (!updateGroup.exec()) {
1441+
LOG_FAILED_QUERY(updateGroup)
1442+
<< "couldn't reassign canonical for group" << groupId;
1443+
return false; // ScopedTransaction destructor rolls back
1444+
}
1445+
1446+
QSqlQuery updateMeta(m_database);
1447+
updateMeta.prepare(QStringLiteral(
1448+
"UPDATE %1 SET is_canonical = 1 "
1449+
"WHERE track_id = :track_id")
1450+
.arg(kFingerprintTableName));
1451+
updateMeta.bindValue(QStringLiteral(":track_id"),
1452+
newCanonical.toVariant());
1453+
if (!updateMeta.exec()) {
1454+
LOG_FAILED_QUERY(updateMeta)
1455+
<< "couldn't promote new canonical in fingerprint_metadata";
1456+
return false; // ScopedTransaction destructor rolls back
1457+
}
1458+
1459+
if (sDebugTrackFingerprintDao) {
1460+
qDebug() << "TrackFingerprintDao -> [clearFingerprintData] -> "
1461+
"promoted new canonical"
1462+
<< newCanonical
1463+
<< "for group" << groupId;
1464+
}
1465+
} else {
1466+
// No other members — delete the now-empty group.
1467+
QSqlQuery deleteGroup(m_database);
1468+
deleteGroup.prepare(QStringLiteral(
1469+
"DELETE FROM %1 WHERE group_id = :group_id")
1470+
.arg(kCmrtGroupsTableName));
1471+
deleteGroup.bindValue(QStringLiteral(":group_id"), groupId);
1472+
if (!deleteGroup.exec()) {
1473+
LOG_FAILED_QUERY(deleteGroup)
1474+
<< "couldn't delete empty cmrt_group" << groupId;
1475+
return false; // ScopedTransaction destructor rolls back
1476+
}
1477+
1478+
if (sDebugTrackFingerprintDao) {
1479+
qDebug() << "TrackFingerprintDao -> [clearFingerprintData] -> "
1480+
"deleted empty group"
1481+
<< groupId;
1482+
}
1483+
}
1484+
}
1485+
}
1486+
1487+
// Step 3 — delete cmrt_members row.
1488+
{
1489+
QSqlQuery q(m_database);
1490+
q.prepare(QStringLiteral("DELETE FROM %1 WHERE track_id = :track_id")
1491+
.arg(kCmrtMembersTableName));
1492+
q.bindValue(QStringLiteral(":track_id"), trackId.toVariant());
1493+
if (!q.exec()) {
1494+
LOG_FAILED_QUERY(q) << "couldn't delete cmrt_member for track" << trackId;
1495+
return false; // ScopedTransaction destructor rolls back
1496+
}
1497+
}
1498+
1499+
// Step 4 — delete fingerprint_metadata row.
1500+
{
1501+
QSqlQuery q(m_database);
1502+
q.prepare(QStringLiteral("DELETE FROM %1 WHERE track_id = :track_id")
1503+
.arg(kFingerprintTableName));
1504+
q.bindValue(QStringLiteral(":track_id"), trackId.toVariant());
1505+
if (!q.exec()) {
1506+
LOG_FAILED_QUERY(q) << "couldn't delete fingerprint_metadata for track" << trackId;
1507+
return false; // ScopedTransaction destructor rolls back
1508+
}
1509+
}
1510+
1511+
// Step 5 — delete acoustid_queue row.
1512+
{
1513+
QSqlQuery q(m_database);
1514+
q.prepare(QStringLiteral("DELETE FROM %1 WHERE track_id = :track_id")
1515+
.arg(kAcoustIdQueueTableName));
1516+
q.bindValue(QStringLiteral(":track_id"), trackId.toVariant());
1517+
if (!q.exec()) {
1518+
LOG_FAILED_QUERY(q)
1519+
<< "couldn't delete acoustid_queue entry for track" << trackId;
1520+
return false; // ScopedTransaction destructor rolls back
1521+
}
1522+
}
1523+
1524+
if (!transaction.commit()) {
1525+
qDebug() << "TrackFingerprintDao -> [clearFingerprintData] -> "
1526+
"failed to commit transaction for trackId:"
1527+
<< trackId;
1528+
return false;
1529+
}
1530+
1531+
if (sDebugTrackFingerprintDao) {
1532+
qDebug() << "TrackFingerprintDao -> [clearFingerprintData] -> done"
1533+
<< "trackId:" << trackId;
1534+
}
1535+
return true;
1536+
}
1537+
1538+
int TrackFingerprintDao::clearAllFingerprintData() const {
1539+
if (sDebugTrackFingerprintDao) {
1540+
qDebug() << "TrackFingerprintDao -> [clearAllFingerprintData] -> entry";
1541+
}
1542+
1543+
if (!m_database.isOpen()) {
1544+
qDebug() << "TrackFingerprintDao -> [clearAllFingerprintData] -> "
1545+
"aborting: database not open";
1546+
return 0;
1547+
}
1548+
1549+
// Collect all track IDs with a fingerprint_metadata row first,
1550+
// then clear each one individually. Clearing inside the SELECT loop
1551+
// would mutate the table being iterated — unsafe with SQLite.
1552+
QSqlQuery q(m_database);
1553+
q.prepare(QStringLiteral("SELECT track_id FROM %1").arg(kFingerprintTableName));
1554+
if (!q.exec()) {
1555+
LOG_FAILED_QUERY(q) << "couldn't fetch fingerprinted track IDs";
1556+
return 0;
1557+
}
1558+
1559+
QList<TrackId> trackIds;
1560+
while (q.next()) {
1561+
const TrackId id(q.value(0));
1562+
if (id.isValid()) {
1563+
trackIds.append(id);
1564+
}
1565+
}
1566+
1567+
int cleared = 0;
1568+
for (const TrackId& id : std::as_const(trackIds)) {
1569+
if (clearFingerprintData(id)) {
1570+
++cleared;
1571+
}
1572+
}
1573+
1574+
if (sDebugTrackFingerprintDao) {
1575+
qDebug() << "TrackFingerprintDao -> [clearAllFingerprintData] -> done"
1576+
<< "cleared:" << cleared << "of" << trackIds.size();
1577+
}
1578+
return cleared;
1579+
}

src/library/dao/trackfingerprintdao.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@ class TrackFingerprintDao : public DAO {
173173
// Sets status='queued', attempts=0, error_message=NULL, last_attempt=NULL.
174174
bool reQueueJob(TrackId trackId) const;
175175

176+
// Clears all fingerprint data for a single track:
177+
// deletes the .chroma file, handles canonical reassignment or group
178+
// deletion in cmrt_groups, removes cmrt_members, fingerprint_metadata,
179+
// and acoustid_queue rows for this track.
180+
// Returns true if the cleanup completed without errors.
181+
bool clearFingerprintData(TrackId trackId) const;
182+
183+
// Calls clearFingerprintData() for every track that has a
184+
// fingerprint_metadata row. Returns the number of tracks cleared.
185+
// Used by the "Clear All Fingerprints" button in Preferences.
186+
int clearAllFingerprintData() const;
187+
176188
private:
177189
// Returns ~/.mixxx/fingerprints/ — creates the directory on first call.
178190
QDir getFingerprintStoragePath() const;

0 commit comments

Comments
 (0)