![]() |
Mixxx
|
00001 // basetrackcache.cpp 00002 // Created 7/3/2011 by RJ Ryan (rryan@mit.edu) 00003 00004 #include "library/basetrackcache.h" 00005 00006 #include "library/trackcollection.h" 00007 00008 namespace { 00009 00010 const bool sDebug = false; 00011 00012 const QHash<QString, int> buildReverseIndex(const QList<QString> items) { 00013 int i = 0; 00014 QHash<QString, int> index; 00015 foreach (const QString item, items) { 00016 index[item] = i++; 00017 } 00018 return index; 00019 } 00020 00021 } // namespace 00022 00023 BaseTrackCache::BaseTrackCache(TrackCollection* pTrackCollection, 00024 QString tableName, 00025 QString idColumn, 00026 QList<QString> columns, 00027 bool isCaching) 00028 : QObject(), 00029 m_tableName(tableName), 00030 m_idColumn(idColumn), 00031 m_columns(columns), 00032 m_columnsJoined(m_columns.join(",")), 00033 m_columnIndex(buildReverseIndex(m_columns)), 00034 m_bIndexBuilt(false), 00035 m_bIsCaching(isCaching), 00036 m_pTrackCollection(pTrackCollection), 00037 m_trackDAO(m_pTrackCollection->getTrackDAO()), 00038 m_database(m_pTrackCollection->getDatabase()) { 00039 m_searchColumns << "artist" 00040 << "album" 00041 << "location" 00042 << "comment" 00043 << "title" 00044 << "genre"; 00045 00046 // Convert all the search column names to their field indexes because we use 00047 // them a bunch. 00048 m_searchColumnIndices.resize(m_searchColumns.size()); 00049 for (int i = 0; i < m_searchColumns.size(); ++i) { 00050 m_searchColumnIndices[i] = m_columnIndex.value(m_searchColumns[i], -1); 00051 } 00052 } 00053 00054 BaseTrackCache::~BaseTrackCache() { 00055 } 00056 00057 const QStringList BaseTrackCache::columns() const { 00058 return m_columns; 00059 } 00060 00061 int BaseTrackCache::columnCount() const { 00062 return m_columns.size(); 00063 } 00064 00065 int BaseTrackCache::fieldIndex(const QString columnName) const { 00066 return m_columnIndex.value(columnName, -1); 00067 } 00068 00069 void BaseTrackCache::slotTracksAdded(QSet<int> trackIds) { 00070 if (sDebug) { 00071 qDebug() << this << "slotTracksAdded" << trackIds.size(); 00072 } 00073 updateTracksInIndex(trackIds); 00074 } 00075 00076 void BaseTrackCache::slotTracksRemoved(QSet<int> trackIds) { 00077 if (sDebug) { 00078 qDebug() << this << "slotTracksRemoved" << trackIds.size(); 00079 } 00080 foreach (int trackId, trackIds) { 00081 m_trackInfo.remove(trackId); 00082 } 00083 } 00084 00085 void BaseTrackCache::slotTrackDirty(int trackId) { 00086 if (sDebug) { 00087 qDebug() << this << "slotTrackDirty" << trackId; 00088 } 00089 m_dirtyTracks.insert(trackId); 00090 } 00091 00092 void BaseTrackCache::slotTrackChanged(int trackId) { 00093 if (sDebug) { 00094 qDebug() << this << "slotTrackChanged" << trackId; 00095 } 00096 QSet<int> trackIds; 00097 trackIds.insert(trackId); 00098 emit(tracksChanged(trackIds)); 00099 } 00100 00101 void BaseTrackCache::slotTrackClean(int trackId) { 00102 if (sDebug) { 00103 qDebug() << this << "slotTrackClean" << trackId; 00104 } 00105 m_dirtyTracks.remove(trackId); 00106 updateTrackInIndex(trackId); 00107 } 00108 00109 bool BaseTrackCache::isCached(int trackId) const { 00110 return m_trackInfo.contains(trackId); 00111 } 00112 00113 void BaseTrackCache::ensureCached(int trackId) { 00114 updateTrackInIndex(trackId); 00115 } 00116 00117 void BaseTrackCache::ensureCached(QSet<int> trackIds) { 00118 updateTracksInIndex(trackIds); 00119 } 00120 00121 TrackPointer BaseTrackCache::lookupCachedTrack(int trackId) const { 00122 // Only get the Track from the TrackDAO if it's in the cache 00123 if (m_bIsCaching) { 00124 return m_trackDAO.getTrack(trackId, true); 00125 } 00126 return TrackPointer(); 00127 } 00128 00129 bool BaseTrackCache::updateIndexWithQuery(QString queryString) { 00130 QTime timer; 00131 timer.start(); 00132 00133 if (sDebug) { 00134 qDebug() << "updateIndexWithQuery issuing query:" << queryString; 00135 } 00136 00137 QSqlQuery query(m_database); 00138 // This causes a memory savings since QSqlCachedResult (what QtSQLite uses) 00139 // won't allocate a giant in-memory table that we won't use at all. 00140 query.setForwardOnly(true); // performance improvement? 00141 query.prepare(queryString); 00142 00143 if (!query.exec()) { 00144 qDebug() << this << "updateIndexWithQuery error:" 00145 << __FILE__ << __LINE__ 00146 << query.executedQuery() << query.lastError(); 00147 return false; 00148 } 00149 00150 int numColumns = columnCount(); 00151 int idColumn = query.record().indexOf(m_idColumn); 00152 00153 while (query.next()) { 00154 int id = query.value(idColumn).toInt(); 00155 00156 QVector<QVariant>& record = m_trackInfo[id]; 00157 record.resize(numColumns); 00158 00159 for (int i = 0; i < numColumns; ++i) { 00160 record[i] = query.value(i); 00161 } 00162 } 00163 00164 qDebug() << this << "updateIndexWithQuery took" << timer.elapsed() << "ms"; 00165 return true; 00166 } 00167 00168 void BaseTrackCache::buildIndex() { 00169 if (sDebug) { 00170 qDebug() << this << "buildIndex()"; 00171 } 00172 00173 QString queryString = QString("SELECT %1 FROM %2") 00174 .arg(m_columnsJoined, m_tableName); 00175 00176 if (sDebug) { 00177 qDebug() << this << "buildIndex query:" << queryString; 00178 } 00179 00180 // TODO(rryan) for very large tables, it probably makes more sense to NOT 00181 // clear the table, and keep track of what IDs we see, then delete the ones 00182 // we don't see. 00183 m_trackInfo.clear(); 00184 00185 if (!updateIndexWithQuery(queryString)) { 00186 qDebug() << "buildIndex failed!"; 00187 } 00188 00189 m_bIndexBuilt = true; 00190 } 00191 00192 void BaseTrackCache::updateTrackInIndex(int trackId) { 00193 QSet<int> trackIds; 00194 trackIds.insert(trackId); 00195 updateTracksInIndex(trackIds); 00196 } 00197 00198 void BaseTrackCache::updateTracksInIndex(QSet<int> trackIds) { 00199 if (trackIds.size() == 0) { 00200 return; 00201 } 00202 00203 QStringList idStrings; 00204 foreach (int trackId, trackIds) { 00205 idStrings << QVariant(trackId).toString(); 00206 } 00207 00208 QString queryString = QString("SELECT %1 FROM %2 WHERE %3 in (%4)") 00209 .arg(m_columnsJoined, m_tableName, m_idColumn, idStrings.join(",")); 00210 00211 if (sDebug) { 00212 qDebug() << this << "updateTracksInIndex update query:" << queryString; 00213 } 00214 00215 if (!updateIndexWithQuery(queryString)) { 00216 qDebug() << "updateTracksInIndex failed!"; 00217 return; 00218 } 00219 emit(tracksChanged(trackIds)); 00220 } 00221 00222 QVariant BaseTrackCache::getTrackValueForColumn(TrackPointer pTrack, int column) const { 00223 if (!pTrack || column < 0) { 00224 return QVariant(); 00225 } 00226 00227 // TODO(XXX) Qt properties could really help here. 00228 // TODO(rryan) this is all TrackDAO specific. What about iTunes/RB/etc.? 00229 if (fieldIndex(LIBRARYTABLE_ARTIST) == column) { 00230 return QVariant(pTrack->getArtist()); 00231 } else if (fieldIndex(LIBRARYTABLE_TITLE) == column) { 00232 return QVariant(pTrack->getTitle()); 00233 } else if (fieldIndex(LIBRARYTABLE_ALBUM) == column) { 00234 return QVariant(pTrack->getAlbum()); 00235 } else if (fieldIndex(LIBRARYTABLE_YEAR) == column) { 00236 return QVariant(pTrack->getYear()); 00237 } else if (fieldIndex(LIBRARYTABLE_DATETIMEADDED) == column) { 00238 return QVariant(pTrack->getDateAdded()); 00239 } else if (fieldIndex(LIBRARYTABLE_GENRE) == column) { 00240 return QVariant(pTrack->getGenre()); 00241 } else if (fieldIndex(LIBRARYTABLE_FILETYPE) == column) { 00242 return QVariant(pTrack->getType()); 00243 } else if (fieldIndex(LIBRARYTABLE_TRACKNUMBER) == column) { 00244 return QVariant(pTrack->getTrackNumber()); 00245 } else if (fieldIndex(LIBRARYTABLE_LOCATION) == column) { 00246 return QVariant(pTrack->getLocation()); 00247 } else if (fieldIndex(LIBRARYTABLE_COMMENT) == column) { 00248 return QVariant(pTrack->getComment()); 00249 } else if (fieldIndex(LIBRARYTABLE_DURATION) == column) { 00250 return pTrack->getDuration(); 00251 } else if (fieldIndex(LIBRARYTABLE_BITRATE) == column) { 00252 return QVariant(pTrack->getBitrate()); 00253 } else if (fieldIndex(LIBRARYTABLE_BPM) == column) { 00254 return QVariant(pTrack->getBpm()); 00255 } else if (fieldIndex(LIBRARYTABLE_PLAYED) == column) { 00256 return QVariant(pTrack->getPlayed()); 00257 } else if (fieldIndex(LIBRARYTABLE_TIMESPLAYED) == column) { 00258 return QVariant(pTrack->getTimesPlayed()); 00259 } else if (fieldIndex(LIBRARYTABLE_RATING) == column) { 00260 return pTrack->getRating(); 00261 } else if (fieldIndex(LIBRARYTABLE_KEY) == column) { 00262 return pTrack->getKey(); 00263 } 00264 return QVariant(); 00265 } 00266 00267 QVariant BaseTrackCache::data(int trackId, int column) const { 00268 QVariant result; 00269 00270 if (!m_bIndexBuilt) { 00271 qDebug() << this << "ERROR index is not built for" << m_tableName; 00272 return result; 00273 } 00274 00275 // TODO(rryan): allow as an argument 00276 TrackPointer pTrack; 00277 00278 // The caller can optionally provide a pTrack if they already looked it 00279 // up. This is just an optimization to help reduce the # of calls to 00280 // lookupCachedTrack. If they didn't provide it, look it up. 00281 if (!pTrack) { 00282 pTrack = lookupCachedTrack(trackId); 00283 } 00284 if (pTrack) { 00285 result = getTrackValueForColumn(pTrack, column); 00286 } 00287 00288 // If the track lookup failed (could happen for track properties we dont 00289 // keep track of in Track, like playlist position) look up the value in 00290 // the track info cache. 00291 00292 // TODO(rryan) this code is flawed for columns that contains row-specific 00293 // metadata. Currently the upper-levels will not delegate row-specific 00294 // columns to this method, but there should still be a check here I think. 00295 if (!result.isValid()) { 00296 QHash<int, QVector<QVariant> >::const_iterator it = 00297 m_trackInfo.find(trackId); 00298 if (it != m_trackInfo.end()) { 00299 const QVector<QVariant>& fields = it.value(); 00300 result = fields.value(column, result); 00301 } 00302 } 00303 return result; 00304 } 00305 00306 bool BaseTrackCache::trackMatches(const TrackPointer& pTrack, 00307 const QRegExp& matcher) const { 00308 // For every search column, lookup the value for the track and check 00309 // if it matches the search query. 00310 int i = 0; 00311 foreach (QString column, m_searchColumns) { 00312 int columnIndex = m_searchColumnIndices[i++]; 00313 QVariant value = getTrackValueForColumn(pTrack, columnIndex); 00314 if (value.isValid() && qVariantCanConvert<QString>(value)) { 00315 QString valueStr = value.toString(); 00316 if (valueStr.contains(matcher)) { 00317 return true; 00318 } 00319 } 00320 } 00321 return false; 00322 } 00323 00324 void BaseTrackCache::filterAndSort(const QSet<int>& trackIds, 00325 QString searchQuery, 00326 QString extraFilter, int sortColumn, 00327 Qt::SortOrder sortOrder, 00328 QHash<int, int>* trackToIndex) { 00329 if (!m_bIndexBuilt) { 00330 buildIndex(); 00331 } 00332 00333 QStringList idStrings; 00334 00335 if (sortColumn < 0 || sortColumn >= columnCount()) { 00336 qDebug() << "ERROR: Invalid sort column provided to BaseTrackCache::filterAndSort"; 00337 return; 00338 } 00339 00340 // TODO(rryan) consider making this the data passed in and a separate 00341 // QVector for output 00342 QSet<int> dirtyTracks; 00343 foreach (int trackId, trackIds) { 00344 idStrings << QVariant(trackId).toString(); 00345 if (m_dirtyTracks.contains(trackId)) { 00346 dirtyTracks.insert(trackId); 00347 } 00348 } 00349 00350 QString filter = filterClause(searchQuery, extraFilter, idStrings); 00351 QString orderBy = orderByClause(sortColumn, sortOrder); 00352 QString queryString = QString("SELECT %1 FROM %2 %3 %4") 00353 .arg(m_idColumn, m_tableName, filter, orderBy); 00354 00355 if (sDebug) { 00356 qDebug() << this << "select() executing:" << queryString; 00357 } 00358 00359 QSqlQuery query(m_database); 00360 // This causes a memory savings since QSqlCachedResult (what QtSQLite uses) 00361 // won't allocate a giant in-memory table that we won't use at all. 00362 query.setForwardOnly(true); 00363 query.prepare(queryString); 00364 00365 if (!query.exec()) { 00366 qDebug() << this << "select() error:" << __FILE__ << __LINE__ 00367 << query.executedQuery() << query.lastError(); 00368 } 00369 00370 QSqlRecord record = query.record(); 00371 int idColumn = record.indexOf(m_idColumn); 00372 int rows = query.size(); 00373 00374 if (sDebug) { 00375 qDebug() << "Rows returned:" << rows; 00376 } 00377 00378 m_trackOrder.resize(0); 00379 trackToIndex->clear(); 00380 if (rows > 0) { 00381 trackToIndex->reserve(rows); 00382 m_trackOrder.reserve(rows); 00383 } 00384 00385 while (query.next()) { 00386 int id = query.value(idColumn).toInt(); 00387 (*trackToIndex)[id] = m_trackOrder.size(); 00388 m_trackOrder.push_back(id); 00389 } 00390 00391 // At this point, the original set of tracks have been divided into two 00392 // pieces: those that should be in the result set and those that should 00393 // not. Unfortunately, due to TrackDAO caching, there may be tracks in 00394 // either category that are there incorrectly. We must look at all the dirty 00395 // tracks (within the original set, if specified) and evaluate whether they 00396 // would match or not match the given filter criteria. Once we correct the 00397 // membership of tracks in either set, we must then insertion-sort the 00398 // missing tracks into the resulting index list. 00399 00400 if (dirtyTracks.size() == 0) { 00401 return; 00402 } 00403 00404 // Make a regular expression that matches the query terms. 00405 QStringList searchTokens = searchQuery.split(" "); 00406 // Escape every token to stuff in a regular expression 00407 for (int i = 0; i < searchTokens.size(); ++i) { 00408 searchTokens[i] = QRegExp::escape(searchTokens[i].trimmed()); 00409 } 00410 QRegExp searchMatcher(searchTokens.join("|"), Qt::CaseInsensitive); 00411 00412 foreach (int trackId, dirtyTracks) { 00413 // Only get the track if it is in the cache. 00414 TrackPointer pTrack = lookupCachedTrack(trackId); 00415 00416 if (!pTrack) { 00417 continue; 00418 } 00419 00420 // The track should be in the result set if the search is empty or the 00421 // track matches the search. 00422 bool shouldBeInResultSet = searchQuery.isEmpty() || 00423 trackMatches(pTrack, searchMatcher); 00424 00425 // If the track is in this result set. 00426 bool isInResultSet = trackToIndex->contains(trackId); 00427 00428 if (shouldBeInResultSet) { 00429 // Track should be in result set... 00430 00431 // Remove the track from the results first (we have to do this or it 00432 // will sort wrong). 00433 if (isInResultSet) { 00434 int index = (*trackToIndex)[trackId]; 00435 m_trackOrder.remove(index); 00436 // Don't update trackToIndex, since we do it below. 00437 } 00438 00439 // Figure out where it is supposed to sort. The table is sorted by 00440 // the sort column, so we can binary search. 00441 int insertRow = findSortInsertionPoint(pTrack, sortColumn, 00442 sortOrder, m_trackOrder); 00443 00444 if (sDebug) { 00445 qDebug() << this 00446 << "Insertion sort says it should be inserted at:" 00447 << insertRow; 00448 } 00449 00450 // The track should sort at insertRow 00451 m_trackOrder.insert(insertRow, trackId); 00452 00453 trackToIndex->clear(); 00454 // Fix the index. TODO(rryan) find a non-stupid way to do this. 00455 for (int i = 0; i < m_trackOrder.size(); ++i) { 00456 (*trackToIndex)[m_trackOrder[i]] = i; 00457 } 00458 } else if (isInResultSet) { 00459 // Track should not be in this result set, but it is. We need to 00460 // remove it. 00461 int index = (*trackToIndex)[trackId]; 00462 m_trackOrder.remove(index); 00463 00464 trackToIndex->clear(); 00465 // Fix the index. TODO(rryan) find a non-stupid way to do this. 00466 for (int i = 0; i < m_trackOrder.size(); ++i) { 00467 (*trackToIndex)[m_trackOrder[i]] = i; 00468 } 00469 } 00470 } 00471 } 00472 00473 00474 QString BaseTrackCache::filterClause(QString query, QString extraFilter, 00475 QStringList idStrings) const { 00476 QStringList queryFragments; 00477 00478 if (!extraFilter.isNull() && extraFilter != "") { 00479 queryFragments << QString("(%1)").arg(extraFilter); 00480 } 00481 00482 if (idStrings.size() > 0) { 00483 queryFragments << QString("%1 in (%2)") 00484 .arg(m_idColumn, idStrings.join(",")); 00485 } 00486 00487 if (!query.isNull() && query != "") { 00488 QStringList tokens = query.split(" "); 00489 QSqlField search("search", QVariant::String); 00490 00491 QStringList tokenFragments; 00492 foreach (QString token, tokens) { 00493 token = token.trimmed(); 00494 search.setValue("%" + token + "%"); 00495 QString escapedToken = m_database.driver()->formatValue(search); 00496 00497 QStringList columnFragments; 00498 foreach (QString column, m_searchColumns) { 00499 columnFragments << QString("%1 LIKE %2").arg(column, escapedToken); 00500 } 00501 tokenFragments << QString("(%1)").arg(columnFragments.join(" OR ")); 00502 } 00503 queryFragments << QString("(%1)").arg(tokenFragments.join(" AND ")); 00504 } 00505 00506 if (queryFragments.size() > 0) { 00507 return "WHERE " + queryFragments.join(" AND "); 00508 } 00509 return ""; 00510 } 00511 00512 QString BaseTrackCache::orderByClause(int sortColumn, 00513 Qt::SortOrder sortOrder) const { 00514 // This is all stolen from QSqlTableModel::orderByClause(), just rigged to 00515 // sort case-insensitively. 00516 00517 // TODO(rryan) I couldn't get QSqlRecord to work without exec'ing this damn 00518 // query. Need to find out how to make it work without exec()'ing and remove 00519 // this. 00520 QSqlQuery query(m_database); 00521 QString queryString = QString("SELECT %1 FROM %2 LIMIT 1") 00522 .arg(m_columnsJoined, m_tableName); 00523 query.prepare(queryString); 00524 query.exec(); 00525 00526 QString s; 00527 QSqlField f = query.record().field(sortColumn); 00528 if (!f.isValid()) { 00529 if (sDebug) { 00530 qDebug() << "field not valid"; 00531 } 00532 return QString(); 00533 } 00534 00535 QString field = m_database.driver()->escapeIdentifier( 00536 f.name(), QSqlDriver::FieldName); 00537 00538 s.append(QLatin1String("ORDER BY ")); 00539 QString sort_field = QString("%1.%2").arg(m_tableName, field); 00540 00541 // If the field is a string, sort using its lowercase form so sort is 00542 // case-insensitive. 00543 QVariant::Type type = f.type(); 00544 00545 // TODO(XXX) Instead of special-casing tracknumber here, we should ask the 00546 // child class to format the expression for sorting. 00547 if (sort_field.contains("tracknumber")) { 00548 sort_field = QString("cast(%1 as integer)").arg(sort_field); 00549 } else if (type == QVariant::String) { 00550 sort_field = QString("lower(%1)").arg(sort_field); 00551 } 00552 s.append(sort_field); 00553 00554 s += (sortOrder == Qt::AscendingOrder) ? QLatin1String(" ASC") : 00555 QLatin1String(" DESC"); 00556 return s; 00557 } 00558 00559 int BaseTrackCache::findSortInsertionPoint(TrackPointer pTrack, 00560 const int sortColumn, 00561 Qt::SortOrder sortOrder, 00562 const QVector<int> trackIds) const { 00563 QVariant trackValue = getTrackValueForColumn(pTrack, sortColumn); 00564 00565 int min = 0; 00566 int max = trackIds.size()-1; 00567 00568 if (sDebug) { 00569 qDebug() << this << "Trying to insertion sort:" 00570 << trackValue << "min" << min << "max" << max; 00571 } 00572 00573 while (min <= max) { 00574 int mid = min + (max - min) / 2; 00575 int otherTrackId = trackIds[mid]; 00576 00577 // This should not happen, but it's a recoverable error so we should only log it. 00578 if (!m_trackInfo.contains(otherTrackId)) { 00579 qDebug() << "WARNING: track" << otherTrackId << "was not in index"; 00580 //updateTrackInIndex(otherTrackId); 00581 } 00582 00583 QVariant tableValue = data(otherTrackId, sortColumn); 00584 int compare = compareColumnValues(sortColumn, sortOrder, trackValue, tableValue); 00585 00586 if (sDebug) { 00587 qDebug() << this << "Comparing" << trackValue 00588 << "to" << tableValue << ":" << compare; 00589 } 00590 00591 if (compare == 0) { 00592 // Alright, if we're here then we can insert it here and be 00593 // "correct" 00594 min = mid; 00595 break; 00596 } else if (compare > 0) { 00597 min = mid + 1; 00598 } else { 00599 max = mid - 1; 00600 } 00601 } 00602 return min; 00603 } 00604 00605 int BaseTrackCache::compareColumnValues(int sortColumn, Qt::SortOrder sortOrder, 00606 QVariant val1, QVariant val2) const { 00607 int result = 0; 00608 00609 if (sortColumn == fieldIndex(PLAYLISTTRACKSTABLE_POSITION) || 00610 sortColumn == fieldIndex(LIBRARYTABLE_BITRATE) || 00611 sortColumn == fieldIndex(LIBRARYTABLE_BPM) || 00612 sortColumn == fieldIndex(LIBRARYTABLE_DURATION) || 00613 sortColumn == fieldIndex(LIBRARYTABLE_TIMESPLAYED) || 00614 sortColumn == fieldIndex(LIBRARYTABLE_RATING)) { 00615 // Sort as floats. 00616 double delta = val1.toDouble() - val2.toDouble(); 00617 00618 if (fabs(delta) < .00001) 00619 result = 0; 00620 else if (delta > 0.0) 00621 result = 1; 00622 else 00623 result = -1; 00624 } else { 00625 // Default to case-insensitive string comparison 00626 result = val1.toString().compare(val2.toString(), Qt::CaseInsensitive); 00627 } 00628 00629 // If we're in descending order, flip the comparison. 00630 if (sortOrder == Qt::DescendingOrder) { 00631 result = -result; 00632 } 00633 00634 return result; 00635 }