@@ -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+ }
0 commit comments