1 #include "library/basesqltablemodel.h"
2
3 #include <QUrl>
4 #include <QtDebug>
5 #include <algorithm>
6
7 #include "library/dao/trackschema.h"
8 #include "library/queryutil.h"
9 #include "library/starrating.h"
10 #include "library/trackcollection.h"
11 #include "library/trackcollectionmanager.h"
12 #include "mixer/playermanager.h"
13 #include "moc_basesqltablemodel.cpp"
14 #include "track/keyutils.h"
15 #include "track/track.h"
16 #include "track/trackmetadata.h"
17 #include "util/assert.h"
18 #include "util/datetime.h"
19 #include "util/db/dbconnection.h"
20 #include "util/duration.h"
21 #include "util/performancetimer.h"
22 #include "util/platform.h"
23
24 namespace {
25
26 const bool sDebug = false;
27
28 // The logic in the following code relies to a track column = 0
29 // Do not change it without changing the logic
30 // Column 0 is skipped when calculating the the columns of the view table
31 const int kIdColumn = 0;
32 const int kMaxSortColumns = 3;
33
34 // Constant for getModelSetting(name)
35 const QString COLUMNS_SORTING = QStringLiteral("ColumnsSorting");
36
37 } // anonymous namespace
38
BaseSqlTableModel(QObject * parent,TrackCollectionManager * pTrackCollectionManager,const char * settingsNamespace)39 BaseSqlTableModel::BaseSqlTableModel(
40 QObject* parent,
41 TrackCollectionManager* pTrackCollectionManager,
42 const char* settingsNamespace)
43 : BaseTrackTableModel(
44 settingsNamespace,
45 pTrackCollectionManager,
46 parent),
47 m_pTrackCollectionManager(pTrackCollectionManager),
48 m_database(pTrackCollectionManager->internalCollection()->database()),
49 m_bInitialized(false) {
50 }
51
~BaseSqlTableModel()52 BaseSqlTableModel::~BaseSqlTableModel() {
53 }
54
initHeaderProperties()55 void BaseSqlTableModel::initHeaderProperties() {
56 BaseTrackTableModel::initHeaderProperties();
57 // Add playlist columns
58 setHeaderProperties(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION,
59 tr("#"),
60 30);
61 setHeaderProperties(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_DATETIMEADDED,
62 tr("Timestamp"),
63 80);
64 }
65
initSortColumnMapping()66 void BaseSqlTableModel::initSortColumnMapping() {
67 // Add a bijective mapping between the SortColumnIds and column indices
68 for (int i = 0; i < static_cast<int>(TrackModel::SortColumnId::IdMax); ++i) {
69 m_columnIndexBySortColumnId[i] = -1;
70 }
71 m_columnIndexBySortColumnId[static_cast<int>(
72 TrackModel::SortColumnId::Artist)] =
73 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST);
74 m_columnIndexBySortColumnId[static_cast<int>(
75 TrackModel::SortColumnId::Title)] =
76 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE);
77 m_columnIndexBySortColumnId[static_cast<int>(
78 TrackModel::SortColumnId::Album)] =
79 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM);
80 m_columnIndexBySortColumnId[static_cast<int>(
81 TrackModel::SortColumnId::AlbumArtist)] =
82 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST);
83 m_columnIndexBySortColumnId[static_cast<int>(
84 TrackModel::SortColumnId::Year)] =
85 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR);
86 m_columnIndexBySortColumnId[static_cast<int>(
87 TrackModel::SortColumnId::Genre)] =
88 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE);
89 m_columnIndexBySortColumnId[static_cast<int>(
90 TrackModel::SortColumnId::Composer)] =
91 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER);
92 m_columnIndexBySortColumnId[static_cast<int>(
93 TrackModel::SortColumnId::Grouping)] =
94 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING);
95 m_columnIndexBySortColumnId[static_cast<int>(
96 TrackModel::SortColumnId::TrackNumber)] =
97 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER);
98 m_columnIndexBySortColumnId[static_cast<int>(
99 TrackModel::SortColumnId::FileType)] =
100 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE);
101 m_columnIndexBySortColumnId[static_cast<int>(
102 TrackModel::SortColumnId::NativeLocation)] =
103 fieldIndex(ColumnCache::COLUMN_TRACKLOCATIONSTABLE_LOCATION);
104 m_columnIndexBySortColumnId[static_cast<int>(
105 TrackModel::SortColumnId::Comment)] =
106 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT);
107 m_columnIndexBySortColumnId[static_cast<int>(
108 TrackModel::SortColumnId::Duration)] =
109 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION);
110 m_columnIndexBySortColumnId[static_cast<int>(
111 TrackModel::SortColumnId::BitRate)] =
112 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE);
113 m_columnIndexBySortColumnId[static_cast<int>(
114 TrackModel::SortColumnId::Bpm)] =
115 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM);
116 m_columnIndexBySortColumnId[static_cast<int>(
117 TrackModel::SortColumnId::ReplayGain)] =
118 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN);
119 m_columnIndexBySortColumnId[static_cast<int>(
120 TrackModel::SortColumnId::DateTimeAdded)] =
121 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED);
122 m_columnIndexBySortColumnId[static_cast<int>(
123 TrackModel::SortColumnId::TimesPlayed)] =
124 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED);
125 m_columnIndexBySortColumnId[static_cast<int>(
126 TrackModel::SortColumnId::Rating)] =
127 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING);
128 m_columnIndexBySortColumnId[static_cast<int>(
129 TrackModel::SortColumnId::Key)] =
130 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY);
131 m_columnIndexBySortColumnId[static_cast<int>(
132 TrackModel::SortColumnId::Preview)] =
133 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW);
134 m_columnIndexBySortColumnId[static_cast<int>(
135 TrackModel::SortColumnId::Color)] =
136 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR);
137 m_columnIndexBySortColumnId[static_cast<int>(
138 TrackModel::SortColumnId::CoverArt)] =
139 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART);
140 m_columnIndexBySortColumnId[static_cast<int>(
141 TrackModel::SortColumnId::SampleRate)] =
142 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_SAMPLERATE);
143
144 m_sortColumnIdByColumnIndex.clear();
145 for (int i = static_cast<int>(TrackModel::SortColumnId::IdMin);
146 i < static_cast<int>(TrackModel::SortColumnId::IdMax);
147 ++i) {
148 TrackModel::SortColumnId sortColumn = static_cast<TrackModel::SortColumnId>(i);
149 m_sortColumnIdByColumnIndex.insert(
150 m_columnIndexBySortColumnId[static_cast<int>(sortColumn)],
151 sortColumn);
152 }
153 }
154
clearRows()155 void BaseSqlTableModel::clearRows() {
156 DEBUG_ASSERT(m_rowInfo.empty() == m_trackIdToRows.empty());
157 DEBUG_ASSERT(m_rowInfo.size() >= m_trackIdToRows.size());
158 if (!m_rowInfo.isEmpty()) {
159 beginRemoveRows(QModelIndex(), 0, m_rowInfo.size() - 1);
160 m_rowInfo.clear();
161 m_trackIdToRows.clear();
162 endRemoveRows();
163 }
164 DEBUG_ASSERT(m_rowInfo.isEmpty());
165 DEBUG_ASSERT(m_trackIdToRows.isEmpty());
166 }
167
replaceRows(QVector<RowInfo> && rows,TrackId2Rows && trackIdToRows)168 void BaseSqlTableModel::replaceRows(
169 QVector<RowInfo>&& rows,
170 TrackId2Rows&& trackIdToRows) {
171 // NOTE(uklotzde): Use r-value references for parameters here, because
172 // conceptually those parameters should replace the corresponding internal
173 // member variables. Currently Qt4/5 doesn't support move semantics and
174 // instead prevents unnecessary deep copying by implicit sharing (COW)
175 // behind the scenes. Moving would be more efficient, although implicit
176 // sharing meets all requirements. If Qt will ever add move support for
177 // its container types in the future this code becomes even more efficient.
178 DEBUG_ASSERT(rows.empty() == trackIdToRows.empty());
179 DEBUG_ASSERT(rows.size() >= trackIdToRows.size());
180 if (rows.isEmpty()) {
181 clearRows();
182 } else {
183 beginInsertRows(QModelIndex(), 0, rows.size() - 1);
184 m_rowInfo = rows;
185 m_trackIdToRows = trackIdToRows;
186 endInsertRows();
187 }
188 }
189
select()190 void BaseSqlTableModel::select() {
191 if (!m_bInitialized) {
192 return;
193 }
194 // We should be able to detect when a select() would be a no-op. The DAO's
195 // do not currently broadcast signals for when common things happen. In the
196 // future, we can turn this check on and avoid a lot of needless
197 // select()'s. rryan 9/2011
198 // if (!m_bDirty) {
199 // if (sDebug) {
200 // qDebug() << this << "Skipping non-dirty select()";
201 // }
202 // return;
203 // }
204
205 if (sDebug) {
206 qDebug() << this << "select()";
207 }
208
209 PerformanceTimer time;
210 time.start();
211
212 // Prepare query for id and all columns not in m_trackSource
213 QString queryString = QString("SELECT %1 FROM %2 %3")
214 .arg(m_tableColumns.join(","), m_tableName, m_tableOrderBy);
215
216 if (sDebug) {
217 qDebug() << this << "select() executing:" << queryString;
218 }
219
220 QSqlQuery query(m_database);
221 // This causes a memory savings since QSqlCachedResult (what QtSQLite uses)
222 // won't allocate a giant in-memory table that we won't use at all.
223 query.setForwardOnly(true);
224 if (!query.prepare(queryString)) {
225 LOG_FAILED_QUERY(query);
226 return;
227 }
228 if (!query.exec()) {
229 LOG_FAILED_QUERY(query);
230 return;
231 }
232
233 // Remove all the rows from the table after(!) the query has been
234 // executed successfully. See Bug #1090888.
235 // TODO(rryan) we could edit the table in place instead of clearing it?
236 clearRows();
237
238 // The size of the result set is not known in advance for a
239 // forward-only query, so we cannot reserve memory for rows
240 // in advance.
241 QVector<RowInfo> rowInfos;
242 QSet<TrackId> trackIds;
243 int idColumn = -1;
244 while (query.next()) {
245 QSqlRecord sqlRecord = query.record();
246
247 if (idColumn < 0) {
248 idColumn = sqlRecord.indexOf(m_idColumn);
249 }
250 VERIFY_OR_DEBUG_ASSERT(idColumn >= 0) {
251 qCritical()
252 << "ID column not available in database query results:"
253 << m_idColumn;
254 return;
255 }
256 // TODO(XXX): Can we get rid of the hard-coded assumption that
257 // the the first column always contains the id?
258 DEBUG_ASSERT(idColumn == kIdColumn);
259
260 TrackId trackId(sqlRecord.value(idColumn));
261 trackIds.insert(trackId);
262
263 RowInfo rowInfo;
264 rowInfo.trackId = trackId;
265 // current position defines the ordering
266 rowInfo.order = rowInfos.size();
267 rowInfo.metadata.reserve(sqlRecord.count());
268 for (int i = 0; i < m_tableColumns.size(); ++i) {
269 rowInfo.metadata.push_back(sqlRecord.value(i));
270 }
271 rowInfos.push_back(rowInfo);
272 }
273
274 if (sDebug) {
275 qDebug() << "Rows actually received:" << rowInfos.size();
276 }
277
278 if (m_trackSource) {
279 m_trackSource->filterAndSort(trackIds,
280 m_currentSearch,
281 m_currentSearchFilter,
282 m_trackSourceOrderBy,
283 m_sortColumns,
284 m_tableColumns.size() - 1, // exclude the 1st column with the id
285 &m_trackSortOrder);
286
287 // Re-sort the track IDs since filterAndSort can change their order or mark
288 // them for removal (by setting their row to -1).
289 for (auto& rowInfo : rowInfos) {
290 // If the sort is not a track column then we will sort only to
291 // separate removed tracks (order == -1) from present tracks (order ==
292 // 0). Otherwise we sort by the order that filterAndSort returned to us.
293 if (m_trackSourceOrderBy.isEmpty()) {
294 rowInfo.order = m_trackSortOrder.contains(rowInfo.trackId) ? 0 : -1;
295 } else {
296 rowInfo.order = m_trackSortOrder.value(rowInfo.trackId, -1);
297 }
298 }
299 }
300
301 // RowInfo::operator< sorts by the order field, except -1 is placed at the
302 // end so we can easily slice off rows that are no longer present. Stable
303 // sort is necessary because the tracks may be in pre-sorted order so we
304 // should not disturb that if we are only removing tracks.
305 std::stable_sort(rowInfos.begin(), rowInfos.end());
306
307 TrackId2Rows trackIdToRows;
308 // We expect almost all rows to be valid and that only a few tracks
309 // are contained multiple times in rowInfos (e.g. in history playlists)
310 trackIdToRows.reserve(rowInfos.size());
311 for (int i = 0; i < rowInfos.size(); ++i) {
312 const RowInfo& rowInfo = rowInfos[i];
313
314 if (rowInfo.order == -1) {
315 // We've reached the end of valid rows. Resize rowInfo to cut off
316 // this and all further elements.
317 rowInfos.resize(i);
318 break;
319 }
320 trackIdToRows[rowInfo.trackId].push_back(i);
321 }
322 // The number of unique tracks cannot be greater than the
323 // number of total rows returned by the query
324 DEBUG_ASSERT(trackIdToRows.size() <= rowInfos.size());
325
326 // We're done! Issue the update signals and replace the master maps.
327 replaceRows(
328 std::move(rowInfos),
329 std::move(trackIdToRows));
330 // Both rowInfo and trackIdToRows (might) have been moved and
331 // must not be used afterwards!
332
333 qDebug() << this << "select() took" << time.elapsed().debugMillisWithUnit()
334 << m_rowInfo.size();
335 }
336
setTable(const QString & tableName,const QString & idColumn,const QStringList & tableColumns,QSharedPointer<BaseTrackCache> trackSource)337 void BaseSqlTableModel::setTable(const QString& tableName,
338 const QString& idColumn,
339 const QStringList& tableColumns,
340 QSharedPointer<BaseTrackCache> trackSource) {
341 if (sDebug) {
342 qDebug() << this << "setTable" << tableName << tableColumns << idColumn;
343 }
344 m_tableName = tableName;
345 m_idColumn = idColumn;
346 m_tableColumns = tableColumns;
347
348 if (m_trackSource) {
349 disconnect(m_trackSource.data(),
350 &BaseTrackCache::tracksChanged,
351 this,
352 &BaseSqlTableModel::tracksChanged);
353 }
354 m_trackSource = trackSource;
355 if (m_trackSource) {
356 // It's important that this not be a direct connection, or else the UI
357 // might try to update while a cache operation is in progress, and that
358 // will hit the cache again and cause dangerous reentry cycles
359 // See https://bugs.launchpad.net/mixxx/+bug/1365708
360 // TODO: A better fix is to have cache and trackpointers defer saving
361 // and deleting, so those operations only take place at the top of
362 // the call stack.
363 connect(m_trackSource.data(),
364 &BaseTrackCache::tracksChanged,
365 this,
366 &BaseSqlTableModel::tracksChanged,
367 Qt::QueuedConnection);
368 }
369
370 initTableColumnsAndHeaderProperties(m_tableColumns);
371 initSortColumnMapping();
372
373 m_bInitialized = true;
374 }
375
columnIndexFromSortColumnId(TrackModel::SortColumnId column) const376 int BaseSqlTableModel::columnIndexFromSortColumnId(TrackModel::SortColumnId column) const {
377 if (column == TrackModel::SortColumnId::Invalid) {
378 return -1;
379 }
380
381 return m_columnIndexBySortColumnId[static_cast<int>(column)];
382 }
383
sortColumnIdFromColumnIndex(int index) const384 TrackModel::SortColumnId BaseSqlTableModel::sortColumnIdFromColumnIndex(int index) const {
385 return m_sortColumnIdByColumnIndex.value(index, TrackModel::SortColumnId::Invalid);
386 }
387
currentSearch() const388 const QString BaseSqlTableModel::currentSearch() const {
389 return m_currentSearch;
390 }
391
setSearch(const QString & searchText,const QString & extraFilter)392 void BaseSqlTableModel::setSearch(const QString& searchText, const QString& extraFilter) {
393 if (sDebug) {
394 qDebug() << this << "setSearch" << searchText;
395 }
396
397 bool searchIsDifferent = m_currentSearch.isNull() || m_currentSearch != searchText;
398 bool filterDisabled = (m_currentSearchFilter.isNull() && extraFilter.isNull());
399 bool searchFilterIsDifferent = m_currentSearchFilter != extraFilter;
400
401 if (!searchIsDifferent && (filterDisabled || !searchFilterIsDifferent)) {
402 // Do nothing if the filters are no different.
403 return;
404 }
405
406 m_currentSearch = searchText;
407 m_currentSearchFilter = extraFilter;
408 }
409
search(const QString & searchText,const QString & extraFilter)410 void BaseSqlTableModel::search(const QString& searchText, const QString& extraFilter) {
411 if (sDebug) {
412 qDebug() << this << "search" << searchText;
413 }
414 setSearch(searchText, extraFilter);
415 select();
416 }
417
setSort(int column,Qt::SortOrder order)418 void BaseSqlTableModel::setSort(int column, Qt::SortOrder order) {
419 if (sDebug) {
420 qDebug() << this << "setSort()" << column << order << m_tableColumns;
421 }
422
423 int trackSourceColumnCount = m_trackSource ? m_trackSource->columnCount() : 0;
424
425 if (column < 0 ||
426 column >= trackSourceColumnCount + m_sortColumns.size() - 1) {
427 // -1 because id column is in both tables
428 qWarning() << "BaseSqlTableModel::setSort invalid column:" << column;
429 return;
430 }
431
432 // There's no item to sort already, load from Settings last sort
433 if (m_sortColumns.isEmpty()) {
434 QString val = getModelSetting(COLUMNS_SORTING);
435 QTextStream in(&val);
436
437 while (!in.atEnd()) {
438 int ordI = -1;
439 QString name;
440
441 in >> name >> ordI;
442
443 int col = fieldIndex(name);
444 if (col < 0) {
445 continue;
446 }
447
448 Qt::SortOrder ord;
449 ord = ordI > 0 ? Qt::AscendingOrder : Qt::DescendingOrder;
450
451 m_sortColumns << SortColumn(col, ord);
452 }
453 }
454 if (m_sortColumns.size() > 0 && m_sortColumns.at(0).m_column == column) {
455 // Only the order has changed
456 m_sortColumns.replace(0, SortColumn(column, order));
457 } else {
458 // Remove column if already in history
459 // As reverse loop to not skip an entry when removing the previous
460 for (int i = m_sortColumns.size() - 1; i >= 0; --i) {
461 if (m_sortColumns.at(i).m_column == column) {
462 m_sortColumns.removeAt(i);
463 break;
464 }
465 }
466
467 // set new sort as head and shift out old sort
468 m_sortColumns.prepend(SortColumn(column, order));
469
470 if (m_sortColumns.size() > kMaxSortColumns) {
471 m_sortColumns.removeLast();
472 }
473 }
474
475 // Write new sortColumns order to user settings
476 QString val;
477 QTextStream out(&val);
478 for (SortColumn& sc : m_sortColumns) {
479 QString name;
480 if (sc.m_column > 0 && sc.m_column < m_tableColumns.size()) {
481 name = m_tableColumns[sc.m_column];
482 } else {
483 // ccColumn between 1..x to skip the id column
484 int ccColumn = sc.m_column - m_tableColumns.size() + 1;
485 name = m_trackSource->columnNameForFieldIndex(ccColumn);
486 }
487
488 out << name << " ";
489 out << (sc.m_order == Qt::AscendingOrder ? 1 : -1) << " ";
490 }
491 out.flush();
492 setModelSetting(COLUMNS_SORTING, val);
493
494 if (sDebug) {
495 qDebug() << "setSort() sortColumns:" << val;
496 }
497
498 // we have two selects for sorting, since keeping the select history
499 // across the two selects is hard, we do this only for the trackSource
500 // this is OK, because the columns of the table are virtual in case of
501 // preview column or individual like playlist track number so that we
502 // do not need the history anyway.
503
504 // reset the old order by clauses
505 m_trackSourceOrderBy.clear();
506 m_tableOrderBy.clear();
507
508 if (column > 0 && column < m_tableColumns.size()) {
509 // Table sorting, no history
510 if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW)) {
511 // Random sort easter egg
512 m_tableOrderBy = "ORDER BY RANDOM()";
513 } else {
514 m_tableOrderBy = "ORDER BY ";
515 QString field = m_tableColumns[column];
516 QString sort_field = QString("%1.%2").arg(m_tableName, field);
517 m_tableOrderBy.append(mixxx::DbConnection::collateLexicographically(sort_field));
518 m_tableOrderBy.append((order == Qt::AscendingOrder) ? " ASC" : " DESC");
519 }
520 m_sortColumns.clear();
521 m_sortColumns.prepend(SortColumn(column, order));
522 } else if (m_trackSource) {
523 bool first = true;
524 for (const SortColumn& sc : qAsConst(m_sortColumns)) {
525 QString sort_field;
526 if (sc.m_column < m_tableColumns.size()) {
527 if (sc.m_column == kIdColumn) {
528 sort_field = m_trackSource->columnSortForFieldIndex(kIdColumn);
529 } else if (sc.m_column ==
530 fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW)) {
531 sort_field = "RANDOM()";
532 } else {
533 // we can't sort by other table columns here since primary sort is a track
534 // column: skip
535 continue;
536 }
537 } else {
538 // + 1 to skip id column
539 int ccColumn = sc.m_column - m_tableColumns.size() + 1;
540 sort_field = m_trackSource->columnSortForFieldIndex(ccColumn);
541 }
542 VERIFY_OR_DEBUG_ASSERT(!sort_field.isEmpty()) {
543 continue;
544 }
545
546 m_trackSourceOrderBy.append(first ? "ORDER BY " : ", ");
547 m_trackSourceOrderBy.append(sort_field);
548 m_trackSourceOrderBy.append((sc.m_order == Qt::AscendingOrder) ? " ASC" : " DESC");
549 //qDebug() << m_trackSourceOrderBy;
550 first = false;
551 }
552 }
553 }
554
sort(int column,Qt::SortOrder order)555 void BaseSqlTableModel::sort(int column, Qt::SortOrder order) {
556 if (sDebug) {
557 qDebug() << this << "sort()" << column << order;
558 }
559 setSort(column, order);
560 select();
561 }
562
rowCount(const QModelIndex & parent) const563 int BaseSqlTableModel::rowCount(const QModelIndex& parent) const {
564 int count = parent.isValid() ? 0 : m_rowInfo.size();
565 //qDebug() << "rowCount()" << parent << count;
566 return count;
567 }
568
columnCount(const QModelIndex & parent) const569 int BaseSqlTableModel::columnCount(const QModelIndex& parent) const {
570 VERIFY_OR_DEBUG_ASSERT(!parent.isValid()) {
571 return 0;
572 }
573 // Subtract one from trackSource::columnCount to ignore the id column
574 int count = m_tableColumns.size() +
575 (m_trackSource ? m_trackSource->columnCount() - 1 : 0);
576 return count;
577 }
578
fieldIndex(ColumnCache::Column column) const579 int BaseSqlTableModel::fieldIndex(ColumnCache::Column column) const {
580 int tableIndex = BaseTrackTableModel::fieldIndex(column);
581 if (tableIndex >= 0) {
582 return tableIndex;
583 }
584 if (m_trackSource) {
585 // We need to account for the case where the field name is not a table
586 // column or a source column.
587 int sourceTableIndex = m_trackSource->fieldIndex(column);
588 if (sourceTableIndex > -1) {
589 // Subtract one from the fieldIndex() result to account for the id column
590 return m_tableColumns.size() + sourceTableIndex - 1;
591 }
592 }
593 return tableIndex;
594 }
595
fieldIndex(const QString & fieldName) const596 int BaseSqlTableModel::fieldIndex(const QString& fieldName) const {
597 int tableIndex = BaseTrackTableModel::fieldIndex(fieldName);
598 if (tableIndex >= 0) {
599 return tableIndex;
600 }
601 if (m_trackSource) {
602 // We need to account for the case where the field name is not a table
603 // column or a source column.
604 int sourceTableIndex = m_trackSource->fieldIndex(fieldName);
605 if (sourceTableIndex > -1) {
606 // Subtract one from the fieldIndex() result to account for the id column
607 return m_tableColumns.size() + sourceTableIndex - 1;
608 }
609 }
610 return tableIndex;
611 }
612
rawValue(const QModelIndex & index) const613 QVariant BaseSqlTableModel::rawValue(
614 const QModelIndex& index) const {
615 DEBUG_ASSERT(index.isValid());
616
617 const int row = index.row();
618 DEBUG_ASSERT(row >= 0);
619 if (row >= m_rowInfo.size()) {
620 return QVariant();
621 }
622
623 const int column = index.column();
624 DEBUG_ASSERT(column >= 0);
625 // TODO(rryan) check range on column
626
627 const RowInfo& rowInfo = m_rowInfo[row];
628 const TrackId trackId = rowInfo.trackId;
629
630 // If the row info has the row-specific column, return that.
631 if (column < m_tableColumns.size()) {
632 // Special case for preview column. Return whether trackId is the
633 // current preview deck track.
634 if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW)) {
635 return previewDeckTrackId() == trackId;
636 }
637
638 const QVector<QVariant>& columns = rowInfo.metadata;
639 if (sDebug) {
640 qDebug() << "Returning table-column value"
641 << columns.at(column)
642 << "for column" << column;
643 }
644 return columns[column];
645 }
646
647 // Otherwise, return the information from the track record cache for the
648 // given track ID
649 if (!m_trackSource) {
650 return QVariant();
651 }
652 // Subtract table columns from index to get the track source column
653 // number and add 1 to skip over the id column.
654 int trackSourceColumn = column - m_tableColumns.size() + 1;
655 if (!m_trackSource->isCached(trackId)) {
656 // Ideally Mixxx would have notified us of this via a signal, but in
657 // the case that a track is not in the cache, we attempt to load it
658 // on the fly. This will be a steep penalty to pay if there are tons
659 // of these tracks in the table that are not cached.
660 qDebug() << __FILE__ << __LINE__
661 << "Track" << trackId
662 << "was not present in cache and had to be manually fetched.";
663 m_trackSource->ensureCached(trackId);
664 }
665 return m_trackSource->data(trackId, trackSourceColumn);
666 }
667
setTrackValueForColumn(const TrackPointer & pTrack,int column,const QVariant & value,int role)668 bool BaseSqlTableModel::setTrackValueForColumn(
669 const TrackPointer& pTrack,
670 int column,
671 const QVariant& value,
672 int role) {
673 if (role != Qt::EditRole) {
674 return false;
675 }
676 // You can't set something in the table columns because we have no way of
677 // persisting it.
678 if (column < m_tableColumns.size()) {
679 return false;
680 }
681
682 // TODO(XXX) Qt properties could really help here.
683 DEBUG_ASSERT(pTrack);
684 if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST) == column) {
685 pTrack->setArtist(value.toString());
686 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE) == column) {
687 pTrack->setTitle(value.toString());
688 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM) == column) {
689 pTrack->setAlbum(value.toString());
690 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST) == column) {
691 pTrack->setAlbumArtist(value.toString());
692 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR) == column) {
693 pTrack->setYear(value.toString());
694 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE) == column) {
695 pTrack->setGenre(value.toString());
696 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER) == column) {
697 pTrack->setComposer(value.toString());
698 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING) == column) {
699 pTrack->setGrouping(value.toString());
700 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER) == column) {
701 pTrack->setTrackNumber(value.toString());
702 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT) == column) {
703 pTrack->setComment(value.toString());
704 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM) == column) {
705 pTrack->trySetBpm(static_cast<double>(value.toDouble()));
706 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED) == column) {
707 // Update both the played flag and the number of times played
708 pTrack->updatePlayCounter(value.toBool());
709 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED) == column) {
710 const int timesPlayed = value.toInt();
711 if (0 < timesPlayed) {
712 // Preserve the played flag and only set the number of times played
713 PlayCounter playCounter(pTrack->getPlayCounter());
714 playCounter.setTimesPlayed(timesPlayed);
715 pTrack->setPlayCounter(playCounter);
716 } else {
717 // Reset both the played flag and the number of times played
718 pTrack->resetPlayCounter();
719 }
720 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING) == column) {
721 StarRating starRating = value.value<StarRating>();
722 pTrack->setRating(starRating.starCount());
723 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY) == column) {
724 pTrack->setKeyText(value.toString(),
725 mixxx::track::io::key::USER);
726 } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK) == column) {
727 pTrack->setBpmLocked(value.toBool());
728 } else {
729 // We never should get up to this point!
730 VERIFY_OR_DEBUG_ASSERT(false) {
731 qWarning() << "Column"
732 << columnNameForFieldIndex(column)
733 << "is not editable!";
734 }
735 return false;
736 }
737 return true;
738 }
739
getTrack(const QModelIndex & index) const740 TrackPointer BaseSqlTableModel::getTrack(const QModelIndex& index) const {
741 return m_pTrackCollectionManager->internalCollection()->getTrackById(getTrackId(index));
742 }
743
getTrackId(const QModelIndex & index) const744 TrackId BaseSqlTableModel::getTrackId(const QModelIndex& index) const {
745 if (index.isValid()) {
746 return TrackId(index.sibling(index.row(), fieldIndex(m_idColumn)).data());
747 } else {
748 return TrackId();
749 }
750 }
751
getTrackLocation(const QModelIndex & index) const752 QString BaseSqlTableModel::getTrackLocation(const QModelIndex& index) const {
753 if (!index.isValid()) {
754 return QString();
755 }
756 QString nativeLocation =
757 index.sibling(index.row(),
758 fieldIndex(ColumnCache::COLUMN_TRACKLOCATIONSTABLE_LOCATION))
759 .data()
760 .toString();
761 return QDir::fromNativeSeparators(nativeLocation);
762 }
763
getCoverInfo(const QModelIndex & index) const764 CoverInfo BaseSqlTableModel::getCoverInfo(const QModelIndex& index) const {
765 CoverInfo coverInfo;
766 coverInfo.hash =
767 index.sibling(index.row(),
768 fieldIndex(ColumnCache::
769 COLUMN_LIBRARYTABLE_COVERART_HASH))
770 .data()
771 .toUInt();
772 coverInfo.type = static_cast<CoverInfo::Type>(
773 index.sibling(index.row(),
774 fieldIndex(ColumnCache::
775 COLUMN_LIBRARYTABLE_COVERART_TYPE))
776 .data()
777 .toInt());
778 coverInfo.source = static_cast<CoverInfo::Source>(
779 index.sibling(index.row(),
780 fieldIndex(ColumnCache::
781 COLUMN_LIBRARYTABLE_COVERART_SOURCE))
782 .data()
783 .toInt());
784 coverInfo.coverLocation =
785 index.sibling(index.row(),
786 fieldIndex(ColumnCache::
787 COLUMN_LIBRARYTABLE_COVERART_LOCATION))
788 .data()
789 .toString();
790 coverInfo.trackLocation = getTrackLocation(index);
791 return coverInfo;
792 }
793
tracksChanged(const QSet<TrackId> & trackIds)794 void BaseSqlTableModel::tracksChanged(const QSet<TrackId>& trackIds) {
795 if (sDebug) {
796 qDebug() << this << "trackChanged" << trackIds.size();
797 }
798
799 const int numColumns = columnCount();
800 for (const auto& trackId : trackIds) {
801 const auto rows = getTrackRows(trackId);
802 for (int row : rows) {
803 //qDebug() << "Row in this result set was updated. Signalling update. track:" << trackId << "row:" << row;
804 QModelIndex topLeft = index(row, 0);
805 QModelIndex bottomRight = index(row, numColumns);
806 emit dataChanged(topLeft, bottomRight);
807 }
808 }
809 }
810
hideTracks(const QModelIndexList & indices)811 void BaseSqlTableModel::hideTracks(const QModelIndexList& indices) {
812 QList<TrackId> trackIds;
813 foreach (QModelIndex index, indices) {
814 TrackId trackId(getTrackId(index));
815 trackIds.append(trackId);
816 }
817
818 m_pTrackCollectionManager->hideTracks(trackIds);
819
820 // TODO(rryan) : do not select, instead route event to BTC and notify from
821 // there.
822 select(); //Repopulate the data model.
823 }
824
getTrackRefs(const QModelIndexList & indices) const825 QList<TrackRef> BaseSqlTableModel::getTrackRefs(
826 const QModelIndexList& indices) const {
827 QList<TrackRef> trackRefs;
828 trackRefs.reserve(indices.size());
829 foreach (QModelIndex index, indices) {
830 trackRefs.append(TrackRef::fromFileInfo(
831 TrackFile(getTrackLocation(index)),
832 getTrackId(index)));
833 }
834 return trackRefs;
835 }
836