-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix detection of moved tracks #2197
Changes from 9 commits
c8d341b
db8707d
4d9857e
d8eee8e
6071d61
3e079c1
69bfe6b
a2477f3
0b2abcb
d2d4359
4700a5b
1bc09e8
726a216
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,12 +33,15 @@ | |
#include "track/tracknumbers.h" | ||
#include "util/assert.h" | ||
#include "util/file.h" | ||
#include "util/logger.h" | ||
#include "util/timer.h" | ||
#include "util/math.h" | ||
|
||
|
||
namespace { | ||
|
||
mixxx::Logger kLogger("TrackDAO"); | ||
|
||
enum { UndefinedRecordIndex = -2 }; | ||
|
||
void markTrackLocationsAsDeleted(QSqlDatabase database, const QString& directory) { | ||
|
@@ -1545,143 +1548,162 @@ bool TrackDAO::detectMovedTracks(QSet<TrackId>* pTracksMovedSetOld, | |
// When it's called from libraryscanner.cpp, there already is a transaction | ||
// started! | ||
|
||
if (!addedTracks.size()) { | ||
if (addedTracks.isEmpty()) { | ||
// We have no moved track. | ||
// We can only guarantee for new tracks that the user has not | ||
// edited metadata, which we have to preserve | ||
// TODO(xxx) resolve old duplicates | ||
return true; | ||
} | ||
|
||
QSqlQuery deletedTrackQuery(m_database); | ||
QSqlQuery newTrackQuery(m_database); | ||
QSqlQuery query(m_database); | ||
QString filename; | ||
// rather use duration then filesize as an indicator of changes. The filesize | ||
// can change by adding more ID3v2 tags | ||
int duration = -1; | ||
|
||
// Query tracks, where we need a successor for | ||
deletedTrackQuery.prepare("SELECT track_locations.id, filename, duration FROM track_locations " | ||
"INNER JOIN library ON track_locations.id=library.location " | ||
"WHERE fs_deleted=1"); | ||
|
||
if (!deletedTrackQuery.exec()) { | ||
LOG_FAILED_QUERY(deletedTrackQuery); | ||
} | ||
|
||
// Query possible successors | ||
// NOTE: Successors are identified by filename and duration (in seconds). | ||
// Since duration is stored as double-precision floating-point and since it | ||
// is sometimes truncated to nearest integer, tolerance of 1 second is used. | ||
newTrackQuery.prepare( | ||
QString("SELECT track_locations.id FROM track_locations " | ||
"INNER JOIN library ON track_locations.id=library.location " | ||
"WHERE track_locations.location IN (%1) AND " | ||
"filename=:filename AND " | ||
"ABS(duration - :duration) < 1").arg( | ||
SqlStringFormatter::formatList(m_database, addedTracks))); | ||
|
||
QSqlRecord queryRecord = deletedTrackQuery.record(); | ||
const int idColumn = queryRecord.indexOf("id"); | ||
const int filenameColumn = queryRecord.indexOf("filename"); | ||
const int durationColumn = queryRecord.indexOf("duration"); | ||
QSqlQuery newTrackQuery(m_database); | ||
newTrackQuery.prepare(QString( | ||
"SELECT library.id as track_id, track_locations.id as location_id, " | ||
"track_locations.location " | ||
"FROM library INNER JOIN track_locations ON library.location=track_locations.id " | ||
"WHERE track_locations.location IN (%1) AND " | ||
"filename=:filename AND " | ||
"ABS(duration - :duration) < 1 AND " | ||
"fs_deleted=0").arg( | ||
SqlStringFormatter::formatList(m_database, addedTracks))); | ||
|
||
// Query tracks, where we need a successor for | ||
QSqlQuery oldTrackQuery(m_database); | ||
oldTrackQuery.prepare( | ||
"SELECT library.id as track_id, track_locations.id as location_id, " | ||
"track_locations.location, filename, duration " | ||
"FROM library INNER JOIN track_locations ON library.location=track_locations.id " | ||
"WHERE fs_deleted=1"); | ||
VERIFY_OR_DEBUG_ASSERT(oldTrackQuery.exec()) { | ||
LOG_FAILED_QUERY(oldTrackQuery); | ||
return false; | ||
} | ||
QSqlRecord oldTrackQueryRecord = oldTrackQuery.record(); | ||
const int oldTrackIdColumn = oldTrackQueryRecord.indexOf("track_id"); | ||
const int oldLocationIdColumn = oldTrackQueryRecord.indexOf("location_id"); | ||
const int oldLocationColumn = oldTrackQueryRecord.indexOf("location"); | ||
const int filenameColumn = oldTrackQueryRecord.indexOf("filename"); | ||
const int durationColumn = oldTrackQueryRecord.indexOf("duration"); | ||
|
||
// For each track that's been "deleted" on disk... | ||
while (deletedTrackQuery.next()) { | ||
while (oldTrackQuery.next()) { | ||
if (*pCancel) { | ||
return false; | ||
} | ||
DbId newTrackLocationId; | ||
DbId oldTrackLocationId(deletedTrackQuery.value(idColumn)); | ||
filename = deletedTrackQuery.value(filenameColumn).toString(); | ||
duration = deletedTrackQuery.value(durationColumn).toInt(); | ||
QString oldTrackLocation = oldTrackQuery.value(oldLocationColumn).toString(); | ||
QString filename = oldTrackQuery.value(filenameColumn).toString(); | ||
// rather use duration then filesize as an indicator of changes. The filesize | ||
// can change by adding more ID3v2 tags | ||
const int duration = oldTrackQuery.value(durationColumn).toInt(); | ||
|
||
qDebug() << "TrackDAO::detectMovedTracks looking for a successor of" << filename << duration; | ||
kLogger.info() | ||
<< "Looking for substitute of missing track location" | ||
<< oldTrackLocation; | ||
|
||
newTrackQuery.bindValue(":filename", filename); | ||
newTrackQuery.bindValue(":duration", duration); | ||
if (!newTrackQuery.exec()) { | ||
// Should not happen! | ||
VERIFY_OR_DEBUG_ASSERT(newTrackQuery.exec()) { | ||
LOG_FAILED_QUERY(newTrackQuery); | ||
continue; | ||
} | ||
// WTF duplicate tracks? | ||
if (newTrackQuery.size() > 1) { | ||
LOG_FAILED_QUERY(newTrackQuery) << "Result size was greater than 1."; | ||
} | ||
|
||
const int query2idColumn = newTrackQuery.record().indexOf("id"); | ||
int newTrackLocationCount = 0; | ||
TrackId newTrackId; | ||
DbId newTrackLocationId; | ||
QString newTrackLocation; | ||
while (newTrackQuery.next()) { | ||
newTrackLocationId = DbId(newTrackQuery.value(query2idColumn)); | ||
newTrackLocation = newTrackQuery.value( | ||
newTrackQuery.record().indexOf("location")).toString(); | ||
VERIFY_OR_DEBUG_ASSERT(newTrackLocation != oldTrackLocation) { | ||
continue; | ||
} | ||
kLogger.info() | ||
<< "Found potential moved track location:" | ||
<< newTrackLocation; | ||
++newTrackLocationCount; | ||
newTrackId = TrackId(newTrackQuery.value( | ||
newTrackQuery.record().indexOf("track_id"))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move the indexOf() part to the top? |
||
newTrackLocationId = DbId(newTrackQuery.value( | ||
newTrackQuery.record().indexOf("location_id"))); | ||
DEBUG_ASSERT(newTrackLocationId.isValid()); | ||
} | ||
switch (newTrackLocationCount) { | ||
case 0: | ||
kLogger.info() | ||
uklotzde marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<< "Found no substitute for missing track location" | ||
<< oldTrackLocation; | ||
continue; | ||
case 1: | ||
DEBUG_ASSERT(newTrackId.isValid()); | ||
DEBUG_ASSERT(newTrackLocationId.isValid()); | ||
kLogger.info() | ||
<< "Found moved track location:" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this potentially join two tracks? |
||
<< oldTrackLocation | ||
<< "->" | ||
<< newTrackLocation; | ||
break; | ||
default: | ||
kLogger.warning() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the case we have tree duplicates and one disappears, or if we have duplicates and one is moved. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this comment. All the new have just been added. If multiple of them are candidates for one missing track, we cannot make a decision. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a possible case:
What will happen? I have not tested it yet. I assume currently both tracks remain missing after the move and pointing to the old location. I think it is better to assign both duplicates to the first of the two moved tracks than break a playlist. |
||
<< "Found too many (" | ||
<< newTrackLocationCount | ||
<< ") potential substitutes for missing track location" | ||
<< oldTrackLocation; | ||
continue; | ||
} | ||
|
||
//If we found a moved track... | ||
if (newTrackLocationId.isValid()) { | ||
qDebug() << "Found moved track!" << filename; | ||
|
||
// Remove old row from track_locations table | ||
query.prepare("DELETE FROM track_locations WHERE id=:id"); | ||
query.bindValue(":id", oldTrackLocationId.toVariant()); | ||
if (!query.exec()) { | ||
// Should not happen! | ||
LOG_FAILED_QUERY(query); | ||
} | ||
TrackId oldTrackId(oldTrackQuery.value(oldTrackIdColumn)); | ||
DEBUG_ASSERT(oldTrackId.isValid()); | ||
DbId oldTrackLocationId(oldTrackQuery.value(oldLocationIdColumn)); | ||
DEBUG_ASSERT(oldTrackLocationId.isValid()); | ||
|
||
// The library scanner will have added a new row to the Library | ||
// table which corresponds to the track in the new location. We need | ||
// to remove that so we don't end up with two rows in the library | ||
// table for the same track. | ||
query.prepare("SELECT id FROM library WHERE location=:location"); | ||
query.bindValue(":location", newTrackLocationId.toVariant()); | ||
if (!query.exec()) { | ||
// Should not happen! | ||
LOG_FAILED_QUERY(query); | ||
} | ||
// The queries ensure that this assertion is always true (fs_deleted=0/1)! | ||
DEBUG_ASSERT(oldTrackId != newTrackId); | ||
|
||
const int query3idColumn = query.record().indexOf("id"); | ||
if (query.next()) { | ||
TrackId newTrackId(query.value(query3idColumn)); | ||
query.prepare("DELETE FROM library WHERE id=:newid"); | ||
query.bindValue(":newid", newTrackLocationId.toVariant()); | ||
if (!query.exec()) { | ||
// Should not happen! | ||
LOG_FAILED_QUERY(query); | ||
} | ||
// We collect all the new tracks the where added to BaseTrackCache as well | ||
pTracksMovedSetNew->insert(newTrackId); | ||
} | ||
// Delete the track | ||
// The library scanner will have added a new row to the Library | ||
// table which corresponds to the track in the new location. We need | ||
// to remove that so we don't end up with two rows in the library | ||
// table for the same track. | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is if we found a duplicate and the track was already part of playlists? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The newly added track can safely be deleted, because it is not referenced in any playlist. The track that was marked as missing will become visible again after referencing the new, existing location. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But if we delete it here and something goes wrong below, the track is lost until the directory hash changes. |
||
QSqlQuery query(m_database); | ||
query.prepare("DELETE FROM library WHERE id=:newid"); | ||
query.bindValue(":newid", newTrackLocationId.toVariant()); | ||
if (!query.exec()) { | ||
// Should not happen! | ||
query.bindValue(":newid", newTrackId.toVariant()); | ||
VERIFY_OR_DEBUG_ASSERT(query.exec()) { | ||
LOG_FAILED_QUERY(query); | ||
} | ||
|
||
} | ||
if (oldTrackLocationId != newTrackLocationId) { | ||
uklotzde marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Update the location foreign key for the existing row in the | ||
// library table to point to the correct row in the track_locations | ||
// table. | ||
query.prepare("SELECT id FROM library WHERE location=:location"); | ||
query.bindValue(":location", oldTrackLocationId.toVariant()); | ||
if (!query.exec()) { | ||
// Should not happen! | ||
LOG_FAILED_QUERY(query); | ||
} | ||
|
||
if (query.next()) { | ||
TrackId oldTrackId(query.value(query3idColumn)); | ||
{ | ||
QSqlQuery query(m_database); | ||
query.prepare("UPDATE library SET location=:newloc WHERE id=:oldid"); | ||
query.bindValue(":newloc", newTrackLocationId.toVariant()); | ||
query.bindValue(":oldid", oldTrackId.toVariant()); | ||
if (!query.exec()) { | ||
// Should not happen! | ||
VERIFY_OR_DEBUG_ASSERT(query.exec()) { | ||
LOG_FAILED_QUERY(query); | ||
// Last chance to skip this entry | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Really? We have already deleted the new track above. |
||
continue; | ||
} | ||
} | ||
// Remove old, orphaned row from track_locations table | ||
{ | ||
QSqlQuery query(m_database); | ||
query.prepare("DELETE FROM track_locations WHERE id=:id"); | ||
query.bindValue(":id", oldTrackLocationId.toVariant()); | ||
VERIFY_OR_DEBUG_ASSERT(query.exec()) { | ||
LOG_FAILED_QUERY(query); | ||
} | ||
|
||
// We collect all the old tracks that has to be updated in BaseTrackCache as well | ||
pTracksMovedSetOld->insert(oldTrackId); | ||
} | ||
} | ||
|
||
// We collect all the old tracks that has to be updated in BaseTrackCache | ||
pTracksMovedSetOld->insert(oldTrackId); | ||
// We collect collect all the new tracks the where added and deleted to BaseTrackCache | ||
pTracksMovedSetNew->insert(newTrackId); | ||
} | ||
return true; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to decide if this shall only detect newly added tracks or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we don't. At least not now. This is how the function was designed and I will not modify it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK.