Mixxx

/home/maxime/Projets/Mixxx/1.10/mixxx/src/library/basetrackcache.cpp

Go to the documentation of this file.
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 }
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Defines