File indexing completed on 2024-05-19 04:56:09

0001 /**
0002  * \file trackdatamodel.cpp
0003  * Model for table with track data.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 15 May 2011
0008  *
0009  * Copyright (C) 2011-2024  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  */
0026 
0027 #include "trackdatamodel.h"
0028 #include "frametablemodel.h"
0029 #include "coretaggedfileiconprovider.h"
0030 
0031 /**
0032  * Constructor.
0033  * @param colorProvider colorProvider
0034  * @param parent parent widget
0035  */
0036 TrackDataModel::TrackDataModel(CoreTaggedFileIconProvider* colorProvider,
0037                                QObject* parent)
0038   : QAbstractTableModel(parent),
0039     m_colorProvider(colorProvider), m_maxDiff(0), m_diffCheckEnabled(false)
0040 {
0041   setObjectName(QLatin1String("TrackDataModel"));
0042 }
0043 
0044 /**
0045  * Get item flags for index.
0046  * @param index model index
0047  * @return item flags
0048  */
0049 Qt::ItemFlags TrackDataModel::flags(const QModelIndex& index) const
0050 {
0051   Qt::ItemFlags theFlags = QAbstractTableModel::flags(index);
0052   if (index.isValid()) {
0053     theFlags |= Qt::ItemIsSelectable | Qt::ItemIsEnabled;
0054     if (static_cast<int>(m_frameTypes.at(index.column()).getType()) <
0055         FT_FirstTrackProperty) {
0056       theFlags |= Qt::ItemIsEditable;
0057     }
0058     if (index.column() == 0) {
0059       theFlags |= Qt::ItemIsUserCheckable;
0060     }
0061   }
0062   return theFlags;
0063 }
0064 
0065 /**
0066  * Get data for a given role.
0067  * @param index model index
0068  * @param role item data role
0069  * @return data for role
0070  */
0071 QVariant TrackDataModel::data(const QModelIndex& index, int role) const
0072 {
0073   if (!index.isValid() ||
0074       index.row() < 0 ||
0075       index.row() >= m_trackDataVector.size() ||
0076       index.column() < 0 ||
0077       index.column() >= m_frameTypes.size())
0078     return QVariant();
0079 
0080   if (role == Qt::DisplayRole || role == Qt::EditRole) {
0081     const ImportTrackData& trackData = m_trackDataVector.at(index.row());
0082     Frame::ExtendedType type = m_frameTypes.at(index.column());
0083     if (auto typeOrProperty = static_cast<int>(type.getType());
0084         typeOrProperty < FT_FirstTrackProperty) {
0085       if (QString value(trackData.getValue(type)); !value.isNull())
0086         return value;
0087     } else {
0088       switch (typeOrProperty) {
0089       case FT_FilePath:
0090         return trackData.getAbsFilename();
0091       case FT_FileName:
0092         return trackData.getFilename();
0093       case FT_Duration:
0094         if (int duration = trackData.getFileDuration()) {
0095           return TaggedFile::formatTime(duration);
0096         }
0097         break;
0098       case FT_ImportDuration:
0099         if (int duration = trackData.getImportDuration()) {
0100           return TaggedFile::formatTime(duration);
0101         }
0102         break;
0103       default:
0104         ;
0105       }
0106     }
0107   } else if (role == FrameTableModel::FrameTypeRole) {
0108     return m_frameTypes.at(index.column()).getType();
0109   } else if (role == Qt::BackgroundRole) {
0110     if (index.column() == 0 && m_diffCheckEnabled) {
0111       const ImportTrackData& trackData = m_trackDataVector.at(index.row());
0112       if (int diff = trackData.getTimeDifference();
0113           diff >= 0 && m_colorProvider) {
0114         return m_colorProvider->colorForContext(diff > m_maxDiff
0115             ? ColorContext::Error : ColorContext::None);
0116       }
0117     }
0118   } else if (role == Qt::CheckStateRole && index.column() == 0) {
0119     return m_trackDataVector.at(index.row()).isEnabled()
0120         ? Qt::Checked : Qt::Unchecked;
0121   }
0122   return QVariant();
0123 }
0124 
0125 /**
0126  * Set data for a given role.
0127  * @param index model index
0128  * @param value data value
0129  * @param role item data role
0130  * @return true if successful
0131  */
0132 bool TrackDataModel::setData(const QModelIndex& index,
0133                               const QVariant& value, int role)
0134 {
0135   if (!index.isValid() ||
0136       index.row() < 0 ||
0137       index.row() >= m_trackDataVector.size() ||
0138       index.column() < 0 ||
0139       index.column() >= m_frameTypes.size())
0140     return false;
0141 
0142   if (role == Qt::EditRole) {
0143     ImportTrackData& trackData = m_trackDataVector[index.row()];
0144     Frame::ExtendedType type = m_frameTypes.at(index.column());
0145     if (static_cast<int>(type.getType()) >= FT_FirstTrackProperty)
0146       return false;
0147 
0148     trackData.setValue(type, value.toString());
0149     return true;
0150   }
0151   if (role == Qt::CheckStateRole && index.column() == 0) {
0152     if (bool isChecked(value.toInt() == Qt::Checked);
0153         isChecked != m_trackDataVector.at(index.row()).isEnabled()) {
0154       m_trackDataVector[index.row()].setEnabled(isChecked);
0155       emit dataChanged(index, index);
0156     }
0157     return true;
0158   }
0159   return false;
0160 }
0161 
0162 /**
0163  * Get data for header section.
0164  * @param section column or row
0165  * @param orientation horizontal or vertical
0166  * @param role item data role
0167  * @return header data for role
0168  */
0169 QVariant TrackDataModel::headerData(
0170     int section, Qt::Orientation orientation, int role) const
0171 {
0172   if (role != Qt::DisplayRole)
0173     return QVariant();
0174   if (orientation == Qt::Horizontal && section < m_frameTypes.size()) {
0175     Frame::ExtendedType type = m_frameTypes.at(section);
0176     if (auto typeOrProperty = static_cast<int>(type.getType());
0177         typeOrProperty < FT_FirstTrackProperty) {
0178       return typeOrProperty == Frame::FT_Track
0179         ? tr("Track") // shorter header for track number
0180         : Frame::getDisplayName(type.getName());
0181     } else {
0182       switch (typeOrProperty) {
0183       case FT_FilePath:
0184         return tr("Absolute path to file");
0185       case FT_FileName:
0186         return tr("Filename");
0187       case FT_Duration:
0188         return tr("Duration");
0189       case FT_ImportDuration:
0190         return tr("Length");
0191       default:
0192         ;
0193       }
0194     }
0195   } else if (orientation == Qt::Vertical && section < m_trackDataVector.size()) {
0196     if (int fileDuration = m_trackDataVector.at(section).getFileDuration();
0197         fileDuration > 0) {
0198       return TaggedFile::formatTime(fileDuration);
0199     }
0200   }
0201   return section + 1;
0202 }
0203 
0204 /**
0205  * Get number of rows.
0206  * @param parent parent model index, invalid for table models
0207  * @return number of rows,
0208  * if parent is valid number of children (0 for table models)
0209  */
0210 int TrackDataModel::rowCount(const QModelIndex& parent) const
0211 {
0212   return parent.isValid() ? 0 : m_trackDataVector.size();
0213 }
0214 
0215 /**
0216  * Get number of columns.
0217  * @param parent parent model index, invalid for table models
0218  * @return number of columns,
0219  * if parent is valid number of children (0 for table models)
0220  */
0221 int TrackDataModel::columnCount(const QModelIndex& parent) const
0222 {
0223   return parent.isValid() ? 0 : m_frameTypes.size();
0224 }
0225 
0226 /**
0227  * Insert rows.
0228  * @param row rows are inserted before this row, if 0 at the begin,
0229  * if rowCount() at the end
0230  * @param count number of rows to insert
0231  * @return true if successful
0232  */
0233 bool TrackDataModel::insertRows(int row, int count, const QModelIndex&)
0234 {
0235   if (count > 0) {
0236     beginInsertRows(QModelIndex(), row, row + count - 1);
0237     m_trackDataVector.insert(row, count, ImportTrackData());
0238     endInsertRows();
0239   }
0240   return true;
0241 }
0242 
0243 /**
0244  * Remove rows.
0245  * @param row rows are removed starting with this row
0246  * @param count number of rows to remove
0247  * @return true if successful
0248  */
0249 bool TrackDataModel::removeRows(int row, int count,
0250                         const QModelIndex&)
0251 {
0252   if (count > 0) {
0253     beginRemoveRows(QModelIndex(), row, row + count - 1);
0254     m_trackDataVector.remove(row, count);
0255     endRemoveRows();
0256   }
0257   return true;
0258 }
0259 
0260 /**
0261  * Insert columns.
0262  * @param column columns are inserted before this column, if 0 at the begin,
0263  * if columnCount() at the end
0264  * @param count number of columns to insert
0265  * @return true if successful
0266  */
0267 bool TrackDataModel::insertColumns(int column, int count,
0268                            const QModelIndex&)
0269 {
0270   if (count > 0) {
0271     beginInsertColumns(QModelIndex(), column, column + count - 1);
0272     for (int i = 0; i < count; ++i)
0273       m_frameTypes.insert(column, Frame::ExtendedType());
0274     endInsertColumns();
0275   }
0276   return true;
0277 }
0278 
0279 /**
0280  * Remove columns.
0281  * @param column columns are removed starting with this column
0282  * @param count number of columns to remove
0283  * @return true if successful
0284  */
0285 bool TrackDataModel::removeColumns(int column, int count,
0286                            const QModelIndex&)
0287 {
0288   if (count > 0) {
0289     beginRemoveColumns(QModelIndex(), column, column + count - 1);
0290     for (int i = 0; i < count; ++i)
0291       m_frameTypes.removeAt(column);
0292     endRemoveColumns();
0293   }
0294   return true;
0295 }
0296 
0297 /**
0298  * Set the check state of all tracks in the table.
0299  *
0300  * @param checked true to check the tracks
0301  */
0302 void TrackDataModel::setAllCheckStates(bool checked)
0303 {
0304   for (int row = 0; row < rowCount(); ++row) {
0305     m_trackDataVector[row].setEnabled(checked);
0306   }
0307 }
0308 
0309 /**
0310  * Set time difference check configuration.
0311  *
0312  * @param enable  true to enable check
0313  * @param maxDiff maximum allowed time difference
0314  */
0315 void TrackDataModel::setTimeDifferenceCheck(bool enable, int maxDiff) {
0316   bool changed = m_diffCheckEnabled != enable || m_maxDiff != maxDiff;
0317   m_diffCheckEnabled = enable;
0318   m_maxDiff = maxDiff;
0319   if (changed)
0320     emit dataChanged(index(0,0), index(rowCount() - 1, 0));
0321 }
0322 
0323 /**
0324  * Calculate accuracy of imported track data.
0325  * @return accuracy in percent, -1 if unknown.
0326  */
0327 int TrackDataModel::calculateAccuracy() const
0328 {
0329   int numImportTracks = 0, numTracks = 0, numMismatches = 0, numMatches = 0;
0330   for (auto it = m_trackDataVector.constBegin();
0331        it != m_trackDataVector.constEnd();
0332        ++it) {
0333     const ImportTrackData& trackData = *it;
0334     if (int diff = trackData.getTimeDifference(); diff >= 0) {
0335       if (diff > 3) {
0336         ++numMismatches;
0337       } else {
0338         ++numMatches;
0339       }
0340     } else {
0341       // no durations available => try to match using file name and title
0342       QSet<QString> titleWords = trackData.getTitleWords();
0343       if (int numWords = titleWords.size(); numWords > 0) {
0344         QSet<QString> fileWords = trackData.getFilenameWords();
0345         if (fileWords.size() < numWords) {
0346           numWords = fileWords.size();
0347         }
0348         if (int wordMatch = numWords > 0
0349               ? 100 * (fileWords & titleWords).size() / numWords : 0;
0350             wordMatch < 75) {
0351           ++numMismatches;
0352         } else {
0353           ++numMatches;
0354         }
0355       }
0356     }
0357     if (trackData.getImportDuration() != 0 || !trackData.getTitle().isEmpty()) {
0358       ++numImportTracks;
0359     }
0360     if (trackData.getFileDuration() != 0) {
0361       ++numTracks;
0362     }
0363   }
0364 
0365   if (numTracks > 0 && numImportTracks > 0 &&
0366       (numMatches > 0 || numMismatches > 0)) {
0367     return numMatches * 100 / numTracks;
0368   }
0369   return -1;
0370 }
0371 
0372 
0373 /**
0374  * Get frame for index.
0375  * @param index model index
0376  * @return frame, 0 if no frame.
0377  */
0378 const Frame* TrackDataModel::getFrameOfIndex(const QModelIndex& index) const
0379 {
0380   if (!index.isValid() ||
0381       index.row() < 0 ||
0382       index.row() >= m_trackDataVector.size() ||
0383       index.column() < 0 ||
0384       index.column() >= m_frameTypes.size())
0385     return nullptr;
0386 
0387   const ImportTrackData& trackData = m_trackDataVector.at(index.row());
0388   Frame::ExtendedType type = m_frameTypes.at(index.column());
0389   if (static_cast<int>(type.getType()) >= FT_FirstTrackProperty)
0390     return nullptr;
0391 
0392   auto it = trackData.findByExtendedType(type);
0393   return it != trackData.cend() ? &(*it) : nullptr;
0394 }
0395 
0396 /**
0397  * Set track data.
0398  * @param trackDataVector track data
0399  */
0400 void TrackDataModel::setTrackData(const ImportTrackDataVector& trackDataVector)
0401 {
0402   static constexpr int initFrameTypes[] = {
0403     FT_ImportDuration, FT_FileName, FT_FilePath,
0404     Frame::FT_Track, Frame::FT_Title,
0405     Frame::FT_Artist, Frame::FT_Album, Frame::FT_Date, Frame::FT_Genre,
0406     Frame::FT_Comment
0407   };
0408 
0409   QList<Frame::ExtendedType> newFrameTypes;
0410   for (auto initFrameType : initFrameTypes) {
0411     newFrameTypes.append( // clazy:exclude=reserve-candidates
0412         Frame::ExtendedType(static_cast<Frame::Type>(initFrameType), QLatin1String("")));
0413   }
0414 
0415   for (auto tit = trackDataVector.constBegin();
0416        tit != trackDataVector.constEnd();
0417        ++tit) {
0418     for (auto fit = tit->cbegin(); fit != tit->cend(); ++fit) {
0419       if (Frame::ExtendedType type = fit->getExtendedType();
0420           type.getType() > Frame::FT_LastV1Frame &&
0421           !newFrameTypes.contains(type)) {
0422         newFrameTypes.append(type);
0423       }
0424     }
0425   }
0426 
0427   int oldNumTypes = m_frameTypes.size();
0428   int newNumTypes = newFrameTypes.size();
0429   int numColumnsChanged = qMin(oldNumTypes, newNumTypes);
0430   if (newNumTypes < oldNumTypes)
0431     beginRemoveColumns(QModelIndex(), newNumTypes, oldNumTypes - 1);
0432   else if (newNumTypes > oldNumTypes)
0433     beginInsertColumns(QModelIndex(), oldNumTypes, newNumTypes - 1);
0434 
0435   m_frameTypes = newFrameTypes;
0436 
0437   if (newNumTypes < oldNumTypes)
0438     endRemoveColumns();
0439   else if (newNumTypes > oldNumTypes)
0440     endInsertColumns();
0441 
0442   int oldNumTracks = m_trackDataVector.size();
0443   int newNumTracks = trackDataVector.size();
0444   int numRowsChanged = qMin(oldNumTracks, newNumTracks);
0445   if (newNumTracks < oldNumTracks)
0446     beginRemoveRows(QModelIndex(), newNumTracks, oldNumTracks - 1);
0447   else if (newNumTracks > oldNumTracks)
0448     beginInsertRows(QModelIndex(), oldNumTracks, newNumTracks - 1);
0449 
0450   m_trackDataVector = trackDataVector;
0451 
0452   if (newNumTracks < oldNumTracks)
0453     endRemoveRows();
0454   else if (newNumTracks > oldNumTracks)
0455     endInsertRows();
0456 
0457 
0458   if (numRowsChanged > 0)
0459     emit dataChanged(
0460           index(0, 0), index(numRowsChanged - 1, numColumnsChanged - 1));
0461 }
0462 
0463 /**
0464  * Get track data.
0465  * @return track data
0466  */
0467 ImportTrackDataVector TrackDataModel::getTrackData() const
0468 {
0469   return m_trackDataVector;
0470 }
0471 
0472 /**
0473  * Get the frame type for a column.
0474  * @param column model column
0475  * @return frame type of Frame::Type or TrackDataModel::TrackProperties,
0476  *         -1 if column invalid.
0477  */
0478 int TrackDataModel::frameTypeForColumn(int column) const
0479 {
0480   return column < m_frameTypes.size() ? m_frameTypes.at(column).getType() : -1;
0481 }
0482 
0483 /**
0484  * Get column for a frame type.
0485  * @param frameType frame type of Frame::Type or
0486  *                  TrackDataModel::TrackProperties.
0487  * @return model column, -1 if not found.
0488  */
0489 int TrackDataModel::columnForFrameType(int frameType) const
0490 {
0491   return m_frameTypes.indexOf(
0492         Frame::ExtendedType(static_cast<Frame::Type>(frameType), QLatin1String("")));
0493 }