1 /**
2  * \file trackdatamodel.cpp
3  * Model for table with track data.
4  *
5  * \b Project: Kid3
6  * \author Urs Fleisch
7  * \date 15 May 2011
8  *
9  * Copyright (C) 2011-2018  Urs Fleisch
10  *
11  * This file is part of Kid3.
12  *
13  * Kid3 is free software; you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation; either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * Kid3 is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
25  */
26 
27 #include "trackdatamodel.h"
28 #include "frametablemodel.h"
29 #include "coretaggedfileiconprovider.h"
30 
31 /**
32  * Constructor.
33  * @param colorProvider colorProvider
34  * @param parent parent widget
35  */
TrackDataModel(CoreTaggedFileIconProvider * colorProvider,QObject * parent)36 TrackDataModel::TrackDataModel(CoreTaggedFileIconProvider* colorProvider,
37                                QObject* parent)
38   : QAbstractTableModel(parent),
39     m_colorProvider(colorProvider), m_maxDiff(0), m_diffCheckEnabled(false)
40 {
41   setObjectName(QLatin1String("TrackDataModel"));
42 }
43 
44 /**
45  * Get item flags for index.
46  * @param index model index
47  * @return item flags
48  */
flags(const QModelIndex & index) const49 Qt::ItemFlags TrackDataModel::flags(const QModelIndex& index) const
50 {
51   Qt::ItemFlags theFlags = QAbstractTableModel::flags(index);
52   if (index.isValid()) {
53     theFlags |= Qt::ItemIsSelectable | Qt::ItemIsEnabled;
54     if (static_cast<int>(m_frameTypes.at(index.column()).getType()) <
55         FT_FirstTrackProperty) {
56       theFlags |= Qt::ItemIsEditable;
57     }
58     if (index.column() == 0) {
59       theFlags |= Qt::ItemIsUserCheckable;
60     }
61   }
62   return theFlags;
63 }
64 
65 /**
66  * Get data for a given role.
67  * @param index model index
68  * @param role item data role
69  * @return data for role
70  */
data(const QModelIndex & index,int role) const71 QVariant TrackDataModel::data(const QModelIndex& index, int role) const
72 {
73   if (!index.isValid() ||
74       index.row() < 0 ||
75       index.row() >= static_cast<int>(m_trackDataVector.size()) ||
76       index.column() < 0 ||
77       index.column() >= static_cast<int>(m_frameTypes.size()))
78     return QVariant();
79 
80   if (role == Qt::DisplayRole || role == Qt::EditRole) {
81     const ImportTrackData& trackData = m_trackDataVector.at(index.row());
82     Frame::ExtendedType type = m_frameTypes.at(index.column());
83     auto typeOrProperty = static_cast<int>(type.getType());
84     if (typeOrProperty < FT_FirstTrackProperty) {
85       QString value(trackData.getValue(type));
86       if (!value.isNull())
87         return value;
88     } else {
89       switch (typeOrProperty) {
90       case FT_FilePath:
91         return trackData.getAbsFilename();
92       case FT_FileName:
93         return trackData.getFilename();
94       case FT_Duration:
95         if (int duration = trackData.getFileDuration()) {
96           return TaggedFile::formatTime(duration);
97         }
98         break;
99       case FT_ImportDuration:
100         if (int duration = trackData.getImportDuration()) {
101           return TaggedFile::formatTime(duration);
102         }
103         break;
104       default:
105         ;
106       }
107     }
108   } else if (role == FrameTableModel::FrameTypeRole) {
109     return m_frameTypes.at(index.column()).getType();
110   } else if (role == Qt::BackgroundRole) {
111     if (index.column() == 0 && m_diffCheckEnabled) {
112       const ImportTrackData& trackData = m_trackDataVector.at(index.row());
113       int diff = trackData.getTimeDifference();
114       if (diff >= 0 && m_colorProvider) {
115         return m_colorProvider->colorForContext(diff > m_maxDiff
116             ? ColorContext::Error : ColorContext::None);
117       }
118     }
119   } else if (role == Qt::CheckStateRole && index.column() == 0) {
120     return m_trackDataVector.at(index.row()).isEnabled()
121         ? Qt::Checked : Qt::Unchecked;
122   }
123   return QVariant();
124 }
125 
126 /**
127  * Set data for a given role.
128  * @param index model index
129  * @param value data value
130  * @param role item data role
131  * @return true if successful
132  */
setData(const QModelIndex & index,const QVariant & value,int role)133 bool TrackDataModel::setData(const QModelIndex& index,
134                               const QVariant& value, int role)
135 {
136   if (!index.isValid() ||
137       index.row() < 0 ||
138       index.row() >= static_cast<int>(m_trackDataVector.size()) ||
139       index.column() < 0 ||
140       index.column() >= static_cast<int>(m_frameTypes.size()))
141     return false;
142 
143   if (role == Qt::EditRole) {
144     ImportTrackData& trackData = m_trackDataVector[index.row()];
145     Frame::ExtendedType type = m_frameTypes.at(index.column());
146     if (static_cast<int>(type.getType()) >= FT_FirstTrackProperty)
147       return false;
148 
149     trackData.setValue(type, value.toString());
150     return true;
151   } else if (role == Qt::CheckStateRole && index.column() == 0) {
152     bool isChecked(value.toInt() == Qt::Checked);
153     if (isChecked != m_trackDataVector.at(index.row()).isEnabled()) {
154       m_trackDataVector[index.row()].setEnabled(isChecked);
155       emit dataChanged(index, index);
156     }
157     return true;
158   }
159   return false;
160 }
161 
162 /**
163  * Get data for header section.
164  * @param section column or row
165  * @param orientation horizontal or vertical
166  * @param role item data role
167  * @return header data for role
168  */
headerData(int section,Qt::Orientation orientation,int role) const169 QVariant TrackDataModel::headerData(
170     int section, Qt::Orientation orientation, int role) const
171 {
172   if (role != Qt::DisplayRole)
173     return QVariant();
174   if (orientation == Qt::Horizontal && section < m_frameTypes.size()) {
175     Frame::ExtendedType type = m_frameTypes.at(section);
176     auto typeOrProperty = static_cast<int>(type.getType());
177     if (typeOrProperty < FT_FirstTrackProperty) {
178       return typeOrProperty == Frame::FT_Track
179         ? tr("Track") // shorter header for track number
180         : Frame::getDisplayName(type.getName());
181     } else {
182       switch (typeOrProperty) {
183       case FT_FilePath:
184         return tr("Absolute path to file");
185       case FT_FileName:
186         return tr("Filename");
187       case FT_Duration:
188         return tr("Duration");
189       case FT_ImportDuration:
190         return tr("Length");
191       default:
192         ;
193       }
194     }
195   } else if (orientation == Qt::Vertical && section < m_trackDataVector.size()) {
196     int fileDuration = m_trackDataVector.at(section).getFileDuration();
197     if (fileDuration > 0) {
198       return TaggedFile::formatTime(fileDuration);
199     }
200   }
201   return section + 1;
202 }
203 
204 /**
205  * Get number of rows.
206  * @param parent parent model index, invalid for table models
207  * @return number of rows,
208  * if parent is valid number of children (0 for table models)
209  */
rowCount(const QModelIndex & parent) const210 int TrackDataModel::rowCount(const QModelIndex& parent) const
211 {
212   return parent.isValid() ? 0 : m_trackDataVector.size();
213 }
214 
215 /**
216  * Get number of columns.
217  * @param parent parent model index, invalid for table models
218  * @return number of columns,
219  * if parent is valid number of children (0 for table models)
220  */
columnCount(const QModelIndex & parent) const221 int TrackDataModel::columnCount(const QModelIndex& parent) const
222 {
223   return parent.isValid() ? 0 : m_frameTypes.size();
224 }
225 
226 /**
227  * Insert rows.
228  * @param row rows are inserted before this row, if 0 at the begin,
229  * if rowCount() at the end
230  * @param count number of rows to insert
231  * @param parent parent model index, invalid for table models
232  * @return true if successful
233  */
insertRows(int row,int count,const QModelIndex &)234 bool TrackDataModel::insertRows(int row, int count, const QModelIndex&)
235 {
236   if (count > 0) {
237     beginInsertRows(QModelIndex(), row, row + count - 1);
238     m_trackDataVector.insert(row, count, ImportTrackData());
239     endInsertRows();
240   }
241   return true;
242 }
243 
244 /**
245  * Remove rows.
246  * @param row rows are removed starting with this row
247  * @param count number of rows to remove
248  * @param parent parent model index, invalid for table models
249  * @return true if successful
250  */
removeRows(int row,int count,const QModelIndex &)251 bool TrackDataModel::removeRows(int row, int count,
252                         const QModelIndex&)
253 {
254   if (count > 0) {
255     beginRemoveRows(QModelIndex(), row, row + count - 1);
256     m_trackDataVector.remove(row, count);
257     endRemoveRows();
258   }
259   return true;
260 }
261 
262 /**
263  * Insert columns.
264  * @param column columns are inserted before this column, if 0 at the begin,
265  * if columnCount() at the end
266  * @param count number of columns to insert
267  * @param parent parent model index, invalid for table models
268  * @return true if successful
269  */
insertColumns(int column,int count,const QModelIndex &)270 bool TrackDataModel::insertColumns(int column, int count,
271                            const QModelIndex&)
272 {
273   if (count > 0) {
274     beginInsertColumns(QModelIndex(), column, column + count - 1);
275     for (int i = 0; i < count; ++i)
276       m_frameTypes.insert(column, Frame::ExtendedType());
277     endInsertColumns();
278   }
279   return true;
280 }
281 
282 /**
283  * Remove columns.
284  * @param column columns are removed starting with this column
285  * @param count number of columns to remove
286  * @param parent parent model index, invalid for table models
287  * @return true if successful
288  */
removeColumns(int column,int count,const QModelIndex &)289 bool TrackDataModel::removeColumns(int column, int count,
290                            const QModelIndex&)
291 {
292   if (count > 0) {
293     beginRemoveColumns(QModelIndex(), column, column + count - 1);
294     for (int i = 0; i < count; ++i)
295       m_frameTypes.removeAt(column);
296     endRemoveColumns();
297   }
298   return true;
299 }
300 
301 /**
302  * Set the check state of all tracks in the table.
303  *
304  * @param checked true to check the tracks
305  */
setAllCheckStates(bool checked)306 void TrackDataModel::setAllCheckStates(bool checked)
307 {
308   for (int row = 0; row < rowCount(); ++row) {
309     m_trackDataVector[row].setEnabled(checked);
310   }
311 }
312 
313 /**
314  * Set time difference check configuration.
315  *
316  * @param enable  true to enable check
317  * @param maxDiff maximum allowed time difference
318  */
setTimeDifferenceCheck(bool enable,int maxDiff)319 void TrackDataModel::setTimeDifferenceCheck(bool enable, int maxDiff) {
320   bool changed = m_diffCheckEnabled != enable || m_maxDiff != maxDiff;
321   m_diffCheckEnabled = enable;
322   m_maxDiff = maxDiff;
323   if (changed)
324     emit dataChanged(index(0,0), index(rowCount() - 1, 0));
325 }
326 
327 /**
328  * Calculate accuracy of imported track data.
329  * @return accuracy in percent, -1 if unknown.
330  */
calculateAccuracy() const331 int TrackDataModel::calculateAccuracy() const
332 {
333   int numImportTracks = 0, numTracks = 0, numMismatches = 0, numMatches = 0;
334   for (auto it = m_trackDataVector.constBegin();
335        it != m_trackDataVector.constEnd();
336        ++it) {
337     const ImportTrackData& trackData = *it;
338     int diff = trackData.getTimeDifference();
339     if (diff >= 0) {
340       if (diff > 3) {
341         ++numMismatches;
342       } else {
343         ++numMatches;
344       }
345     } else {
346       // no durations available => try to match using file name and title
347       QSet<QString> titleWords = trackData.getTitleWords();
348       int numWords = titleWords.size();
349       if (numWords > 0) {
350         QSet<QString> fileWords = trackData.getFilenameWords();
351         if (fileWords.size() < numWords) {
352           numWords = fileWords.size();
353         }
354         int wordMatch = numWords > 0
355             ? 100 * (fileWords & titleWords).size() / numWords : 0;
356         if (wordMatch < 75) {
357           ++numMismatches;
358         } else {
359           ++numMatches;
360         }
361       }
362     }
363     if (trackData.getImportDuration() != 0 || !trackData.getTitle().isEmpty()) {
364       ++numImportTracks;
365     }
366     if (trackData.getFileDuration() != 0) {
367       ++numTracks;
368     }
369   }
370 
371   if (numTracks > 0 && numImportTracks > 0 &&
372       (numMatches > 0 || numMismatches > 0)) {
373     return numMatches * 100 / numTracks;
374   }
375   return -1;
376 }
377 
378 
379 /**
380  * Get frame for index.
381  * @param index model index
382  * @return frame, 0 if no frame.
383  */
getFrameOfIndex(const QModelIndex & index) const384 const Frame* TrackDataModel::getFrameOfIndex(const QModelIndex& index) const
385 {
386   if (!index.isValid() ||
387       index.row() < 0 ||
388       index.row() >= static_cast<int>(m_trackDataVector.size()) ||
389       index.column() < 0 ||
390       index.column() >= static_cast<int>(m_frameTypes.size()))
391     return nullptr;
392 
393   const ImportTrackData& trackData = m_trackDataVector.at(index.row());
394   Frame::ExtendedType type = m_frameTypes.at(index.column());
395   if (static_cast<int>(type.getType()) >= FT_FirstTrackProperty)
396     return nullptr;
397 
398   auto it = trackData.findByExtendedType(type);
399   return it != trackData.cend() ? &(*it) : nullptr;
400 }
401 
402 /**
403  * Set track data.
404  * @param trackDataVector track data
405  */
setTrackData(const ImportTrackDataVector & trackDataVector)406 void TrackDataModel::setTrackData(const ImportTrackDataVector& trackDataVector)
407 {
408   static const int initFrameTypes[] = {
409     FT_ImportDuration, FT_FileName, FT_FilePath,
410     Frame::FT_Track, Frame::FT_Title,
411     Frame::FT_Artist, Frame::FT_Album, Frame::FT_Date, Frame::FT_Genre,
412     Frame::FT_Comment
413   };
414 
415   QList<Frame::ExtendedType> newFrameTypes;
416   for (auto initFrameType : initFrameTypes) {
417     newFrameTypes.append( // clazy:exclude=reserve-candidates
418         Frame::ExtendedType(static_cast<Frame::Type>(initFrameType), QLatin1String("")));
419   }
420 
421   for (auto tit = trackDataVector.constBegin();
422        tit != trackDataVector.constEnd();
423        ++tit) {
424     for (auto fit = tit->cbegin(); fit != tit->cend(); ++fit) {
425       Frame::ExtendedType type = fit->getExtendedType();
426       if (type.getType() > Frame::FT_LastV1Frame &&
427           !newFrameTypes.contains(type)) {
428         newFrameTypes.append(type);
429       }
430     }
431   }
432 
433   int oldNumTypes = m_frameTypes.size();
434   int newNumTypes = newFrameTypes.size();
435   int numColumnsChanged = qMin(oldNumTypes, newNumTypes);
436   if (newNumTypes < oldNumTypes)
437     beginRemoveColumns(QModelIndex(), newNumTypes, oldNumTypes - 1);
438   else if (newNumTypes > oldNumTypes)
439     beginInsertColumns(QModelIndex(), oldNumTypes, newNumTypes - 1);
440 
441   m_frameTypes = newFrameTypes;
442 
443   if (newNumTypes < oldNumTypes)
444     endRemoveColumns();
445   else if (newNumTypes > oldNumTypes)
446     endInsertColumns();
447 
448   int oldNumTracks = m_trackDataVector.size();
449   int newNumTracks = trackDataVector.size();
450   int numRowsChanged = qMin(oldNumTracks, newNumTracks);
451   if (newNumTracks < oldNumTracks)
452     beginRemoveRows(QModelIndex(), newNumTracks, oldNumTracks - 1);
453   else if (newNumTracks > oldNumTracks)
454     beginInsertRows(QModelIndex(), oldNumTracks, newNumTracks - 1);
455 
456   m_trackDataVector = trackDataVector;
457 
458   if (newNumTracks < oldNumTracks)
459     endRemoveRows();
460   else if (newNumTracks > oldNumTracks)
461     endInsertRows();
462 
463 
464   if (numRowsChanged > 0)
465     emit dataChanged(
466           index(0, 0), index(numRowsChanged - 1, numColumnsChanged - 1));
467 }
468 
469 /**
470  * Get track data.
471  * @return track data
472  */
getTrackData() const473 ImportTrackDataVector TrackDataModel::getTrackData() const
474 {
475   return m_trackDataVector;
476 }
477 
478 /**
479  * Get the frame type for a column.
480  * @param column model column
481  * @return frame type of Frame::Type or TrackDataModel::TrackProperties,
482  *         -1 if column invalid.
483  */
frameTypeForColumn(int column) const484 int TrackDataModel::frameTypeForColumn(int column) const
485 {
486   return column < m_frameTypes.size() ? m_frameTypes.at(column).getType() : -1;
487 }
488 
489 /**
490  * Get column for a frame type.
491  * @param frameType frame type of Frame::Type or
492  *                  TrackDataModel::TrackProperties.
493  * @return model column, -1 if not found.
494  */
columnForFrameType(int frameType) const495 int TrackDataModel::columnForFrameType(int frameType) const
496 {
497   return m_frameTypes.indexOf(
498         Frame::ExtendedType(static_cast<Frame::Type>(frameType), QLatin1String("")));
499 }
500