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

0001 /**
0002  * \file taggedfile.cpp
0003  * Handling of tagged files.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 25 Sep 2005
0008  *
0009  * Copyright (C) 2005-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 "taggedfile.h"
0028 #include <QDir>
0029 #include <QString>
0030 #include <QRegularExpression>
0031 #ifdef Q_OS_WIN32
0032 #include <sys/types.h>
0033 #include <sys/utime.h>
0034 #else
0035 #include <utime.h>
0036 #endif
0037 #include <sys/stat.h>
0038 #include "tagconfig.h"
0039 #include "formatconfig.h"
0040 #include "genres.h"
0041 #include "modeliterator.h"
0042 #include "saferename.h"
0043 #include "taggedfilesystemmodel.h"
0044 
0045 /**
0046  * Constructor.
0047  *
0048  * @param idx index in tagged file system model
0049  */
0050 TaggedFile::TaggedFile(const QPersistentModelIndex& idx)
0051   : m_index(idx), m_truncation(0), m_modified(false), m_marked(false)
0052 {
0053   FOR_ALL_TAGS(tagNr) {
0054     m_changedFrames[tagNr] = 0;
0055     m_changed[tagNr] = false;
0056   }
0057   Q_ASSERT(m_index.model()->metaObject() == &TaggedFileSystemModel::staticMetaObject);
0058   if (const TaggedFileSystemModel* model = getTaggedFileSystemModel()) {
0059     m_newFilename = model->fileName(m_index);
0060     m_filename = m_newFilename;
0061   }
0062 }
0063 
0064 /**
0065  * Get tagged file model.
0066  * @return tagged file model.
0067  */
0068 const TaggedFileSystemModel* TaggedFile::getTaggedFileSystemModel() const
0069 {
0070   // The validity of this cast is checked in the constructor.
0071   return static_cast<const TaggedFileSystemModel*>(m_index.model());
0072 }
0073 
0074 /**
0075  * Get directory name.
0076  *
0077  * @return directory name
0078  */
0079 QString TaggedFile::getDirname() const
0080 {
0081   if (const TaggedFileSystemModel* model = getTaggedFileSystemModel()) {
0082     return model->filePath(m_index.parent());
0083   }
0084   return QString();
0085 }
0086 
0087 /**
0088  * Set file name.
0089  *
0090  * @param fn file name
0091  */
0092 void TaggedFile::setFilename(const QString& fn)
0093 {
0094   m_newFilename = fn;
0095   m_revertedFilename.clear();
0096   updateModifiedState();
0097 }
0098 
0099 /**
0100  * Set file name and format it if format while editing is switched on.
0101  *
0102  * @param fn file name
0103  */
0104 void TaggedFile::setFilenameFormattedIfEnabled(QString fn)
0105 {
0106   if (FilenameFormatConfig::instance().formatWhileEditing()) {
0107     FilenameFormatConfig::instance().formatString(fn);
0108   }
0109   setFilename(fn);
0110 }
0111 
0112 /**
0113  * Update the current filename after the file was renamed.
0114  */
0115 void TaggedFile::updateCurrentFilename()
0116 {
0117   if (const TaggedFileSystemModel* model = getTaggedFileSystemModel()) {
0118     if (const QString newName = model->fileName(m_index);
0119         !newName.isEmpty() && m_filename != newName) {
0120       if (m_newFilename == m_filename) {
0121         m_newFilename = newName;
0122       }
0123       m_filename = newName;
0124       updateModifiedState();
0125     }
0126   }
0127 }
0128 
0129 /**
0130  * Get current path to file.
0131  * @return absolute path.
0132  */
0133 QString TaggedFile::currentFilePath() const
0134 {
0135   if (const TaggedFileSystemModel* model = getTaggedFileSystemModel()) {
0136     return model->filePath(m_index);
0137   }
0138   return QString();
0139 }
0140 
0141 /**
0142  * Get features supported.
0143  * @return bit mask with Feature flags set.
0144  */
0145 int TaggedFile::taggedFileFeatures() const
0146 {
0147   return 0;
0148 }
0149 
0150 /**
0151  * Get currently active tagged file features.
0152  * @return active tagged file features.
0153  * @see setActiveTaggedFileFeatures()
0154  */
0155 int TaggedFile::activeTaggedFileFeatures() const
0156 {
0157   return 0;
0158 }
0159 
0160 /**
0161  * Activate some features provided by the tagged file.
0162  * For example, if the TF_ID3v24 feature is provided, it can be set, so that
0163  * writeTags() will write ID3v2.4.0 tags. If the feature is deactivated by
0164  * passing 0, tags in the default format will be written again.
0165  *
0166  * @param features bit mask with some of the Feature flags which are
0167  * provided by this file, as returned by taggedFileFeatures(), 0 to disable
0168  * special features.
0169  */
0170 void TaggedFile::setActiveTaggedFileFeatures(int features)
0171 {
0172   Q_UNUSED(features)
0173 }
0174 
0175 /**
0176  * Remove frames.
0177  *
0178  * @param tagNr tag number
0179  * @param flt filter specifying which frames to remove
0180  */
0181 void TaggedFile::deleteFrames(Frame::TagNumber tagNr, const FrameFilter& flt)
0182 {
0183   Frame frame;
0184   frame.setValue(QLatin1String(""));
0185   for (int i = Frame::FT_FirstFrame; i <= Frame::FT_LastV1Frame; ++i) {
0186     if (auto type = static_cast<Frame::Type>(i); flt.isEnabled(type)) {
0187       frame.setExtendedType(Frame::ExtendedType(type));
0188       setFrame(tagNr, frame);
0189     }
0190   }
0191 }
0192 
0193 /**
0194  * Check if file has an ID3v1 tag.
0195  *
0196  * @return true if a V1 tag is available.
0197  * @see isTagInformationRead()
0198  */
0199 bool TaggedFile::hasTag(Frame::TagNumber) const
0200 {
0201   return false;
0202 }
0203 
0204 /**
0205  * Check if tags are supported by the format of this file.
0206  *
0207  * @param tagNr tag number
0208  * @return true if V1 tags are supported.
0209  */
0210 bool TaggedFile::isTagSupported(Frame::TagNumber tagNr) const
0211 {
0212   return tagNr == Frame::Tag_2;
0213 }
0214 
0215 /**
0216  * Get absolute filename.
0217  *
0218  * @return absolute file path.
0219  */
0220 QString TaggedFile::getAbsFilename() const
0221 {
0222   QDir dir(getDirname());
0223   return QDir::cleanPath(dir.absoluteFilePath(m_newFilename));
0224 }
0225 
0226 /**
0227  * Mark filename as unchanged.
0228  */
0229 void TaggedFile::markFilenameUnchanged()
0230 {
0231   m_filename = m_newFilename;
0232   m_revertedFilename.clear();
0233   updateModifiedState();
0234 }
0235 
0236 /**
0237  * Revert modification of filename.
0238  */
0239 void TaggedFile::revertChangedFilename()
0240 {
0241   m_revertedFilename = m_newFilename;
0242   m_newFilename = m_filename;
0243   updateModifiedState();
0244 }
0245 
0246 /**
0247  * Undo reverted modification of filename.
0248  * When writeTags() fails because the file is not writable, the filename is
0249  * reverted using revertChangedFilename() so that the file permissions can be
0250  * changed using the real filename. After changing the permissions, this
0251  * function can be used to change the filename back before saving the file.
0252  */
0253 void TaggedFile::undoRevertChangedFilename()
0254 {
0255   if (!m_revertedFilename.isEmpty()) {
0256     m_newFilename = m_revertedFilename;
0257     m_revertedFilename.clear();
0258     updateModifiedState();
0259   }
0260 }
0261 
0262 /**
0263  * Mark tag as changed.
0264  *
0265  * @param tagNr tag number
0266  * @param extendedType type of changed frame
0267  */
0268 void TaggedFile::markTagChanged(Frame::TagNumber tagNr,
0269                                 const Frame::ExtendedType& extendedType)
0270 {
0271   Frame::Type type = extendedType.getType();
0272   m_changed[tagNr] = true;
0273   if (static_cast<unsigned>(type) < sizeof(m_changedFrames[tagNr]) * 8) {
0274     m_changedFrames[tagNr] |= 1ULL << type;
0275   }
0276   if (type == Frame::FT_Other) {
0277     if (const QString internalName = extendedType.getInternalName();
0278         !internalName.isEmpty()) {
0279       m_changedOtherFrameNames[tagNr].insert(internalName);
0280     }
0281   }
0282   updateModifiedState();
0283 }
0284 
0285 /**
0286  * Mark tag as unchanged.
0287  * @param tagNr tag number
0288  */
0289 void TaggedFile::markTagUnchanged(Frame::TagNumber tagNr) {
0290   m_changed[tagNr] = false;
0291   m_changedFrames[tagNr] = 0;
0292   m_changedOtherFrameNames[tagNr].clear();
0293   clearTrunctionFlags(tagNr);
0294   updateModifiedState();
0295 }
0296 
0297 /**
0298  * Get the types of the changed frames in a tag.
0299  * @param tagNr tag number
0300  * @return types of changed frames.
0301  */
0302 QList<Frame::ExtendedType> TaggedFile::getChangedFrames(
0303     Frame::TagNumber tagNr) const {
0304   QList<Frame::ExtendedType> types;
0305   if (tagNr < Frame::Tag_NumValues) {
0306     const QSet<QString> changedOtherFrameNames = m_changedOtherFrameNames[tagNr];
0307     const quint64 changedFrames = m_changedFrames[tagNr];
0308     quint64 mask;
0309     int i;
0310     for (i = Frame::FT_FirstFrame, mask = 1ULL;
0311          i <= Frame::FT_LastFrame;
0312          ++i, mask <<= 1) {
0313       if (changedFrames & mask) {
0314         types.append(Frame::ExtendedType(
0315                        static_cast<Frame::Type>(i), QString()));
0316       }
0317     }
0318     if (!changedOtherFrameNames.isEmpty()) {
0319       for (const QString& name : changedOtherFrameNames) {
0320         types.append(Frame::ExtendedType(Frame::FT_Other, name));
0321       }
0322     } else if (changedFrames & (1ULL << Frame::FT_Other)) {
0323       types.append(Frame::ExtendedType(Frame::FT_Other, QString()));
0324     }
0325     if (changedFrames & (1ULL << Frame::FT_UnknownFrame)) {
0326       types.append(Frame::ExtendedType());
0327     }
0328   }
0329   return types;
0330 }
0331 
0332 /**
0333  * Set the types of the changed frames in a tag.
0334  * @param tagNr tag number
0335  * @param types types of changed frames
0336  */
0337 void TaggedFile::setChangedFrames(Frame::TagNumber tagNr,
0338                                   const QList<Frame::ExtendedType>& types) {
0339   quint64& mask = m_changedFrames[tagNr];
0340   QSet<QString>& changedOtherFrameNames = m_changedOtherFrameNames[tagNr];
0341   mask = 0;
0342   changedOtherFrameNames.clear();
0343   for (const auto& extendedType : types) {
0344     Frame::Type type = extendedType.getType();
0345     mask |= 1ULL << type;
0346     if (type == Frame::FT_Other) {
0347       if (const QString internalName = extendedType.getInternalName();
0348           !internalName.isEmpty()) {
0349         changedOtherFrameNames.insert(internalName);
0350       }
0351     }
0352   }
0353   m_changed[tagNr] = mask != 0;
0354   updateModifiedState();
0355 }
0356 
0357 void TaggedFile::updateModifiedState()
0358 {
0359   bool modified = false;
0360   FOR_ALL_TAGS(tagNr) {
0361     if (m_changed[tagNr]) {
0362       modified = true;
0363       break;
0364     }
0365   }
0366   modified = modified || m_newFilename != m_filename;
0367   if (m_modified != modified) {
0368     m_modified = modified;
0369     if (const TaggedFileSystemModel* model = getTaggedFileSystemModel()) {
0370       const_cast<TaggedFileSystemModel*>(model)->notifyModificationChanged(
0371             m_index, m_modified);
0372     }
0373   }
0374 }
0375 
0376 /**
0377  * Notify model about changes in extra model data, e.g. the information on
0378  * which the CoreTaggedFileIconProvider depends.
0379  *
0380  * This method shall be called when such data changes, e.g. at the end of
0381  * readTags() implementations.
0382  *
0383  * @param priorIsTagInformationRead prior value returned by
0384  * isTagInformationRead()
0385  */
0386 void TaggedFile::notifyModelDataChanged(bool priorIsTagInformationRead) const
0387 {
0388   if (isTagInformationRead() != priorIsTagInformationRead) {
0389     if (const TaggedFileSystemModel* model = getTaggedFileSystemModel()) {
0390       const_cast<TaggedFileSystemModel*>(model)->notifyModelDataChanged(m_index);
0391     }
0392   }
0393 }
0394 
0395 /**
0396  * Notify model about changes in the truncation state.
0397  *
0398  * This method shall be called when truncation is checked.
0399  *
0400  * @param priorTruncation prior value of m_truncation != 0
0401  */
0402 void TaggedFile::notifyTruncationChanged(bool priorTruncation) const
0403 {
0404   if (bool currentTruncation = m_truncation != 0;
0405       currentTruncation != priorTruncation) {
0406     if (const TaggedFileSystemModel* model = getTaggedFileSystemModel()) {
0407       const_cast<TaggedFileSystemModel*>(model)->notifyModelDataChanged(m_index);
0408     }
0409   }
0410 }
0411 
0412 
0413 namespace {
0414 
0415 /**
0416  * Remove artist part from album string.
0417  * This is used when only the album is needed, but the regexp in
0418  * getTagsFromFilename() matched a "artist - album" string.
0419  *
0420  * @param album album string
0421  *
0422  * @return album with artist removed.
0423  */
0424 QString removeArtist(const QString& album)
0425 {
0426   QString str(album);
0427   if (int pos = str.indexOf(QLatin1String(" - ")); pos != -1) {
0428     str.remove(0, pos + 3);
0429   }
0430   return str;
0431 }
0432 
0433 }
0434 
0435 /**
0436  * Get tags from filename.
0437  * Supported formats:
0438  * album/track - artist - song
0439  * artist - album/track song
0440  * /artist - album - track - song
0441  * album/artist - track - song
0442  * artist/album/track song
0443  * album/artist - song
0444  *
0445  * @param frames frames to put result
0446  * @param fmt format string containing the following codes:
0447  *            %s title (song)
0448  *            %l album
0449  *            %a artist
0450  *            %c comment
0451  *            %y year
0452  *            %t track
0453  */
0454 void TaggedFile::getTagsFromFilename(FrameCollection& frames, const QString& fmt) const
0455 {
0456   QRegularExpression re;
0457   QRegularExpressionMatch match;
0458   QString fn(getAbsFilename());
0459 
0460   // construct regular expression from format string
0461 
0462   // if the format does not contain a '_', they are replaced by spaces
0463   // in the filename.
0464   QString fileName(fn);
0465   if (!fmt.contains(QLatin1Char('_'))) {
0466     fileName.replace(QLatin1Char('_'), QLatin1Char(' '));
0467   }
0468 
0469   QString pattern;
0470   QMap<QString, int> codePos;
0471   bool useCustomCaptures = fmt.contains(QLatin1String("}("));
0472   if (!useCustomCaptures) {
0473     // escape regexp characters
0474     const int fmtLen = fmt.length();
0475     static const QString escChars(QLatin1String("+?.*^$()[]{}|\\"));
0476     for (int i = 0; i < fmtLen; ++i) {
0477       const QChar ch = fmt.at(i);
0478       if (escChars.contains(ch)) {
0479         pattern += QLatin1Char('\\');
0480       }
0481       pattern += ch;
0482     }
0483   } else {
0484     pattern = fmt;
0485   }
0486 
0487   static const struct {
0488     const char* from;
0489     const char* to;
0490   } codeToName[] = {
0491     { "s", "title" },
0492     { "l", "album" },
0493     { "a", "artist" },
0494     { "c", "comment" },
0495     { "y", "date" },
0496     { "t", "track number" },
0497     { "g", "genre" },
0498     { "year", "date" },
0499     { "track", "track number" },
0500     { "tracknumber", "track number" },
0501     { "discnumber", "disc number" }
0502   };
0503   int percentIdx = 0, nr = 1;
0504   const QString prefix(QLatin1String(useCustomCaptures ? "%{" : "%\\{"));
0505   const QString suffix(QLatin1String(useCustomCaptures ? "}"  : "\\}"));
0506   const int prefixLen = prefix.length();
0507   for (const auto& c2n : codeToName) {
0508     QString from = QString::fromLatin1(c2n.from);
0509     QString to = QString::fromLatin1(c2n.to);
0510     from = from.size() == 1 ? QLatin1Char('%') + from : prefix + from + suffix;
0511     to = prefix + to + suffix;
0512     pattern.replace(from, to);
0513   }
0514 
0515   // remove %{} expressions and insert captures if without custom captures
0516   while ((percentIdx = pattern.indexOf(prefix, percentIdx)) >= 0 &&
0517          percentIdx < pattern.length() - 1) {
0518     if (int closingBracePos = pattern.indexOf(suffix, percentIdx + prefixLen);
0519         closingBracePos > percentIdx + prefixLen) {
0520       QString code = pattern.mid(percentIdx + prefixLen,
0521                                  closingBracePos - percentIdx - prefixLen);
0522       codePos[code] = nr++;
0523       const int braceExprLen = closingBracePos - percentIdx + prefixLen - 1;
0524       if (!useCustomCaptures) {
0525         QString capture(QLatin1String(
0526                           code == QLatin1String("track number")
0527                           ? "([A-Za-z]?\\d+[A-Za-z]?)"
0528                           : code == QLatin1String("date")
0529                             ? "(\\d{1,4}[\\dT :-]*)"
0530                             : code == QLatin1String("disc number") ||
0531                               code == QLatin1String("bpm")
0532                               ? "(\\d{1,4})"
0533                               : "([^-_\\./ ](?:[^/]*[^-_/ ])?)"));
0534         pattern.replace(percentIdx, braceExprLen, capture);
0535         percentIdx += capture.length();
0536       } else {
0537         pattern.remove(percentIdx, braceExprLen);
0538         percentIdx += 2;
0539       }
0540     } else {
0541       percentIdx += prefixLen;
0542     }
0543   }
0544 
0545   if (!useCustomCaptures) {
0546     // accept file names with spaces before the extension
0547     pattern += QLatin1String("\\s*");
0548   }
0549 
0550   // and finally a dot followed by 2 to 4 characters for the extension
0551   pattern += QLatin1String("\\..{2,4}$");
0552 
0553   re.setPattern(pattern);
0554   if ((match = re.match(fileName)).hasMatch()) {
0555     for (auto it = codePos.begin(); it != codePos.end(); ++it) {
0556       const QString& name = it.key();
0557       if (QString str = match.captured(*it); !str.isEmpty()) {
0558         if (!useCustomCaptures && name == QLatin1String("track number") &&
0559             str.length() == 2 && str[0] == QLatin1Char('0')) {
0560           // remove leading zero
0561           str = str.mid(1);
0562         }
0563         if (name != QLatin1String("ignore"))
0564           frames.setValue(Frame::ExtendedType(name), str);
0565       }
0566     }
0567     return;
0568   }
0569 
0570   // album/track - artist - song
0571   re.setPattern(QLatin1String(
0572     R"(([^/]+)/(\d{1,3})[-_\. ]+([^-_\./ ][^/]+)[_ ]-[_ ])"
0573     R"(([^-_\./ ][^/]+)\..{2,4}$)"));
0574   if ((match = re.match(fn)).hasMatch()) {
0575     frames.setAlbum(removeArtist(match.captured(1)));
0576     frames.setTrack(match.captured(2).toInt());
0577     frames.setArtist(match.captured(3));
0578     frames.setTitle(match.captured(4));
0579     return;
0580   }
0581 
0582   // artist - album (year)/track song
0583   re.setPattern(QLatin1String(
0584     R"(([^/]+)[_ ]-[_ ]([^/]+)[_ ]\((\d{4})\)/(\d{1,3})[-_\. ]+)"
0585     R"(([^-_\./ ][^/]+)\..{2,4}$)"));
0586   if ((match = re.match(fn)).hasMatch()) {
0587     frames.setArtist(match.captured(1));
0588     frames.setAlbum(match.captured(2));
0589     frames.setYear(match.captured(3).toInt());
0590     frames.setTrack(match.captured(4).toInt());
0591     frames.setTitle(match.captured(5));
0592     return;
0593   }
0594 
0595   // artist - album/track song
0596   re.setPattern(QLatin1String(
0597     R"(([^/]+)[_ ]-[_ ]([^/]+)/(\d{1,3})[-_\. ]+([^-_\./ ][^/]+)\..{2,4}$)"));
0598   if ((match = re.match(fn)).hasMatch()) {
0599     frames.setArtist(match.captured(1));
0600     frames.setAlbum(match.captured(2));
0601     frames.setTrack(match.captured(3).toInt());
0602     frames.setTitle(match.captured(4));
0603     return;
0604   }
0605   // /artist - album - track - song
0606   re.setPattern(QLatin1String(
0607     R"(/([^/]+[^-_/ ])[_ ]-[_ ]([^-_/ ][^/]+[^-_/ ])[-_\. ]+)"
0608     R"((\d{1,3})[-_\. ]+([^-_\./ ][^/]+)\..{2,4}$)"));
0609   if ((match = re.match(fn)).hasMatch()) {
0610     frames.setArtist(match.captured(1));
0611     frames.setAlbum(match.captured(2));
0612     frames.setTrack(match.captured(3).toInt());
0613     frames.setTitle(match.captured(4));
0614     return;
0615   }
0616   // album/artist - track - song
0617   re.setPattern(QLatin1String(
0618     R"(([^/]+)/([^/]+[^-_\./ ])[-_\. ]+(\d{1,3})[-_\. ]+)"
0619     R"(([^-_\./ ][^/]+)\..{2,4}$)"));
0620   if ((match = re.match(fn)).hasMatch()) {
0621     frames.setAlbum(removeArtist(match.captured(1)));
0622     frames.setArtist(match.captured(2));
0623     frames.setTrack(match.captured(3).toInt());
0624     frames.setTitle(match.captured(4));
0625     return;
0626   }
0627   // artist/album/track song
0628   re.setPattern(QLatin1String(
0629     R"(([^/]+)/([^/]+)/(\d{1,3})[-_\. ]+([^-_\./ ][^/]+)\..{2,4}$)"));
0630   if ((match = re.match(fn)).hasMatch()) {
0631     frames.setArtist(match.captured(1));
0632     frames.setAlbum(match.captured(2));
0633     frames.setTrack(match.captured(3).toInt());
0634     frames.setTitle(match.captured(4));
0635     return;
0636   }
0637   // album/artist - song
0638   re.setPattern(QLatin1String(
0639     "([^/]+)/([^/]+[^-_/ ])[_ ]-[_ ]([^-_/ ][^/]+)\\..{2,4}$"));
0640   if ((match = re.match(fn)).hasMatch()) {
0641     frames.setAlbum(removeArtist(match.captured(1)));
0642     frames.setArtist(match.captured(2));
0643     frames.setTitle(match.captured(3));
0644   }
0645 }
0646 
0647 /**
0648  * Format a time string "h:mm:ss".
0649  * If the time is less than an hour, the hour is not put into the
0650  * string and the minute is not padded with zeroes.
0651  *
0652  * @param seconds time in seconds
0653  *
0654  * @return string with the time in hours, minutes and seconds.
0655  */
0656 QString TaggedFile::formatTime(unsigned seconds)
0657 {
0658   unsigned hours = seconds / 3600;
0659   seconds %= 3600;
0660   unsigned minutes = seconds / 60;
0661   seconds %= 60;
0662   QString timeStr;
0663   if (hours > 0) {
0664     timeStr = QString(QLatin1String("%1:%2:%3"))
0665         .arg(hours)
0666         .arg(minutes, 2, 10, QLatin1Char('0'))
0667         .arg(seconds, 2, 10, QLatin1Char('0'));
0668   } else {
0669     timeStr = QString(QLatin1String("%1:%2"))
0670         .arg(minutes).arg(seconds, 2, 10, QLatin1Char('0'));
0671   }
0672   return timeStr;
0673 }
0674 
0675 /**
0676  * Rename a file.
0677  * This methods takes care of case insensitive filesystems.
0678  * @return true if ok.
0679  */
0680 bool TaggedFile::renameFile() const
0681 {
0682   const QString dirname = getDirname();
0683   const QString fnOld = currentFilename();
0684   const QString fnNew = getFilename();
0685   auto model = const_cast<TaggedFileSystemModel*>(getTaggedFileSystemModel());
0686 
0687   if (fnNew.toLower() == fnOld.toLower()) {
0688     // If the filenames only differ in case, the new file is reported to
0689     // already exist on case insensitive filesystems (e.g. Windows),
0690     // so it is checked if the new file is really the old file by
0691     // comparing inodes and devices. If the files are not the same,
0692     // another file would be overwritten and an error is reported.
0693     if (QFile::exists(dirname + QDir::separator() + fnNew)) {
0694       struct stat statOld, statNew;
0695       if (::stat((dirname + QDir::separator() + fnOld).toLatin1().data(),
0696                  &statOld) == 0 &&
0697           ::stat((dirname + QDir::separator() + fnNew).toLatin1().data(),
0698                  &statNew) == 0 &&
0699           !(statOld.st_ino == statNew.st_ino &&
0700             statOld.st_dev == statNew.st_dev)) {
0701         qDebug("rename(%s, %s): %s already exists", fnOld.toLatin1().data(),
0702                fnNew.toLatin1().data(), fnNew.toLatin1().data());
0703         return false;
0704       }
0705     }
0706 
0707     // if the filenames only differ in case, first rename to a
0708     // temporary filename, so that it works also with case
0709     // insensitive filesystems (e.g. Windows).
0710     QString temp_filename(fnNew);
0711     temp_filename.append(QLatin1String("_CASE"));
0712     if (!((model && model->rename(m_index, temp_filename)) ||
0713           Utils::safeRename(dirname, fnOld, temp_filename))) {
0714       qDebug("rename(%s, %s) failed", fnOld.toLatin1().data(),
0715              temp_filename.toLatin1().data());
0716       return false;
0717     }
0718     if (!((model && model->rename(m_index, fnNew)) ||
0719           Utils::safeRename(dirname, temp_filename, fnNew))) {
0720       qDebug("rename(%s, %s) failed", temp_filename.toLatin1().data(),
0721              fnNew.toLatin1().data());
0722       return false;
0723     }
0724   } else if (QFile::exists(dirname + QDir::separator() + fnNew)) {
0725     qDebug("rename(%s, %s): %s already exists", fnOld.toLatin1().data(),
0726            fnNew.toLatin1().data(), fnNew.toLatin1().data());
0727     return false;
0728   } else if (!((model && model->rename(m_index, fnNew)) ||
0729                Utils::safeRename(dirname, fnOld, fnNew))) {
0730     qDebug("rename(%s, %s) failed", fnOld.toLatin1().data(),
0731            fnNew.toLatin1().data());
0732     return false;
0733   }
0734   return true;
0735 }
0736 
0737 /**
0738  * Get field name for comment from configuration.
0739  *
0740  * @return field name.
0741  */
0742 QString TaggedFile::getCommentFieldName() const
0743 {
0744   return TagConfig::instance().commentName();
0745 }
0746 
0747 /**
0748  * Split a track string into number and total.
0749  *
0750  * @param str track
0751  * @param total the total is returned here if found, else 0
0752  *
0753  * @return number, 0 if parsing failed, -1 if str is null
0754  */
0755 int TaggedFile::splitNumberAndTotal(const QString& str, int* total)
0756 {
0757   if (total)
0758     *total = 0;
0759   if (str.isNull())
0760     return -1;
0761 
0762   int slashPos = str.indexOf(QLatin1Char('/'));
0763   if (slashPos == -1)
0764     return str.toInt();
0765 
0766 #if QT_VERSION >= 0x060000
0767   if (total)
0768     *total = str.mid(slashPos + 1).toInt();
0769   return str.left(slashPos).toInt();
0770 #else
0771   if (total)
0772     *total = str.midRef(slashPos + 1).toInt();
0773   return str.leftRef(slashPos).toInt();
0774 #endif
0775 }
0776 
0777 /**
0778  * Fix up a key to be valid.
0779  * If the key contains new line characters because it is coming from an ID3
0780  * frame (e.g. "COMM - COMMENTS\nDescription"), the description part is taken.
0781  * Illegal characters depending on @a tagType are removed.
0782  *
0783  * @param key key which might have invalid characters.
0784  * @param tagType tag type
0785  * @return key which can be used for tag type.
0786  */
0787 QString TaggedFile::fixUpTagKey(const QString& key, TagType tagType)
0788 {
0789   int len = key.length();
0790   int i = key.indexOf(QLatin1Char('\n'));
0791   if (i < 0) {
0792     // key does not contain '\n' => 0..len
0793     i = 0;
0794   } else if (i >= len - 1) {
0795     // '\n' at end of key => 0..len-1
0796     i = 0;
0797     --len;
0798   } else {
0799     // key contains '\n' at i => i+1..len
0800     ++i;
0801   }
0802 
0803   // Allowed characters depending on tag type:
0804   // TT_Vorbis: != '=' && >= 0x20 && <= 0x7D
0805   // TT_Ape: >= 0x20 && <= 0x7E
0806   QChar forbidden;
0807   QChar firstAllowed;
0808   QChar lastAllowed;
0809   if (tagType == TT_Vorbis) {
0810     forbidden = QLatin1Char('=');
0811     firstAllowed = QLatin1Char('\x20');
0812     lastAllowed = QLatin1Char('\x7d');
0813   } else if (tagType == TT_Ape) {
0814     firstAllowed = QLatin1Char('\x20');
0815     lastAllowed = QLatin1Char('\x7e');
0816   }
0817 
0818   QString result;
0819   result.reserve(len - i);
0820   if (forbidden.isNull() && firstAllowed.isNull() && lastAllowed.isNull()) {
0821     result = key.mid(i, len - i);
0822   } else {
0823     while (i < len) {
0824       if (QChar ch = key.at(i);
0825           ch != forbidden &&
0826           ch >= firstAllowed && ch <= lastAllowed) {
0827         result.append(ch);
0828       }
0829       ++i;
0830     }
0831   }
0832   return result;
0833 }
0834 
0835 /**
0836  * Get the total number of tracks in the directory.
0837  *
0838  * @return total number of tracks, -1 if unavailable.
0839  */
0840 int TaggedFile::getTotalNumberOfTracksInDir() const {
0841   int numTracks = -1;
0842   if (QModelIndex parentIdx = m_index.parent(); parentIdx.isValid()) {
0843     numTracks = 0;
0844     TaggedFileOfDirectoryIterator it(parentIdx);
0845     while (it.hasNext()) {
0846       it.next();
0847       ++numTracks;
0848     }
0849   }
0850   return numTracks;
0851 }
0852 
0853 /**
0854  * Get the total number of tracks if it is enabled.
0855  *
0856  * @return total number of tracks,
0857  *         -1 if disabled or unavailable.
0858  */
0859 int TaggedFile::getTotalNumberOfTracksIfEnabled() const
0860 {
0861   return TagConfig::instance().enableTotalNumberOfTracks()
0862       ? getTotalNumberOfTracksInDir() : -1;
0863 }
0864 
0865 /**
0866  * Format track number/total number of tracks with configured digits.
0867  *
0868  * @param num track number, <= 0 if empty
0869  * @param numTracks total number of tracks, <= 0 to disable
0870  *
0871  * @return formatted "track/total" string.
0872  */
0873 QString TaggedFile::trackNumberString(int num, int numTracks) const
0874 {
0875   int numDigits = getTrackNumberDigits();
0876   QString str;
0877   if (num != 0) {
0878     if (numDigits > 0) {
0879       str = QString(QLatin1String("%1"))
0880           .arg(num, numDigits, 10, QLatin1Char('0'));
0881     } else {
0882       str.setNum(num);
0883     }
0884     if (numTracks > 0) {
0885       str += QLatin1Char('/');
0886       if (numDigits > 0) {
0887         str += QString(QLatin1String("%1"))
0888             .arg(numTracks, numDigits, 10, QLatin1Char('0'));
0889       } else {
0890         str += QString::number(numTracks);
0891       }
0892     }
0893   } else {
0894     str = QLatin1String("");
0895   }
0896   return str;
0897 }
0898 
0899 /**
0900  * Format the track number (digits, total number of tracks) if enabled.
0901  *
0902  * @param value    string containing track number, will be modified
0903  * @param addTotal true to add total number of tracks if enabled
0904  *                 "/t" with t = total number of tracks will be appended
0905  *                 if enabled and value contains a number
0906  */
0907 void TaggedFile::formatTrackNumberIfEnabled(QString& value, bool addTotal) const
0908 {
0909   int numDigits = getTrackNumberDigits();
0910   if (int numTracks = addTotal ? getTotalNumberOfTracksIfEnabled() : -1;
0911       numTracks > 0 || numDigits > 1) {
0912     bool ok;
0913     if (int trackNr = value.toInt(&ok); ok && trackNr > 0) {
0914       if (numTracks > 0) {
0915         value = QString(QLatin1String("%1/%2"))
0916             .arg(trackNr, numDigits, 10, QLatin1Char('0'))
0917             .arg(numTracks, numDigits, 10, QLatin1Char('0'));
0918       } else {
0919         value = QString(QLatin1String("%1"))
0920             .arg(trackNr, numDigits, 10, QLatin1Char('0'));
0921       }
0922     }
0923   }
0924 }
0925 
0926 /**
0927  * Get the number of track number digits configured.
0928  *
0929  * @return track number digits,
0930  *         1 if invalid or unavailable.
0931  */
0932 int TaggedFile::getTrackNumberDigits() const
0933 {
0934   int numDigits = TagConfig::instance().trackNumberDigits();
0935   if (numDigits < 1 || numDigits > 5)
0936     numDigits = 1;
0937   return numDigits;
0938 }
0939 
0940 /**
0941  * Get the format of tag 1.
0942  *
0943  * @return string describing format of tag 1,
0944  *         e.g. "ID3v1.1", "ID3v2.3", "Vorbis", "APE",
0945  *         QString::null if unknown.
0946  */
0947 QString TaggedFile::getTagFormat(Frame::TagNumber) const
0948 {
0949   return QString();
0950 }
0951 
0952 /**
0953  * Check if a string has to be truncated.
0954  *
0955  * @param tagNr tag number
0956  * @param str  string to be checked
0957  * @param flag flag to be set if string has to be truncated
0958  * @param len  maximum length of string
0959  *
0960  * @return str truncated to len characters if necessary, else QString::null.
0961  */
0962 QString TaggedFile::checkTruncation(
0963   Frame::TagNumber tagNr, const QString& str, quint64 flag, int len)
0964 {
0965   if (tagNr != Frame::Tag_Id3v1)
0966     return QString();
0967 
0968   bool priorTruncation = m_truncation != 0;
0969   QString result;
0970   if (str.length() > len) {
0971     result = str;
0972     result.truncate(len);
0973     m_truncation |= flag;
0974   } else {
0975     m_truncation &= ~flag;
0976   }
0977   notifyTruncationChanged(priorTruncation);
0978   return result;
0979 }
0980 
0981 /**
0982  * Check if a number has to be truncated.
0983  *
0984  * @param tagNr tag number
0985  * @param val  value to be checked
0986  * @param flag flag to be set if number has to be truncated
0987  * @param max  maximum value
0988  *
0989  * @return val truncated to max if necessary, else -1.
0990  */
0991 int TaggedFile::checkTruncation(Frame::TagNumber tagNr, int val, quint64 flag,
0992                                 int max)
0993 {
0994   if (tagNr != Frame::Tag_Id3v1)
0995     return -1;
0996 
0997   bool priorTruncation = m_truncation != 0;
0998   int result;
0999   if (val > max) {
1000     m_truncation |= flag;
1001     result = max;
1002   } else {
1003     m_truncation &= ~flag;
1004     result = -1;
1005   }
1006   notifyTruncationChanged(priorTruncation);
1007   return result;
1008 }
1009 
1010 /**
1011  * Add a frame in the tags.
1012  *
1013  * @param tagNr tag number
1014  * @param frame frame to add, a field list may be added by this method
1015  *
1016  * @return true if ok.
1017  */
1018 bool TaggedFile::addFrame(Frame::TagNumber tagNr, Frame& frame)
1019 {
1020   if (tagNr == Frame::Tag_Id3v1)
1021     return false;
1022 
1023   return setFrame(tagNr, frame);
1024 }
1025 
1026 /**
1027  * Delete a frame from the tags.
1028  *
1029  * @param tagNr tag number
1030  * @param frame frame to delete
1031  *
1032  * @return true if ok.
1033  */
1034 bool TaggedFile::deleteFrame(Frame::TagNumber tagNr, const Frame& frame)
1035 {
1036   if (tagNr == Frame::Tag_Id3v1)
1037     return false;
1038 
1039   Frame emptyFrame(frame);
1040   emptyFrame.setValue(QLatin1String(""));
1041   return setFrame(tagNr, emptyFrame);
1042 }
1043 
1044 /**
1045  * Get all frames in tag.
1046  *
1047  * @param tagNr tag number
1048  * @param frames frame collection to set.
1049  */
1050 void TaggedFile::getAllFrames(Frame::TagNumber tagNr, FrameCollection& frames)
1051 {
1052   frames.clear();
1053   Frame frame;
1054   for (int i = Frame::FT_FirstFrame; i <= Frame::FT_LastV1Frame; ++i) {
1055     if (getFrame(tagNr, static_cast<Frame::Type>(i), frame)) {
1056       frames.insert(frame);
1057     }
1058   }
1059 }
1060 
1061 /**
1062  * Update marked property of frames.
1063  * Mark frames which violate configured rules. This method should be called
1064  * in reimplementations of getAllFrames().
1065  *
1066  * @param tagNr tag number
1067  * @param frames frames to check
1068  */
1069 void TaggedFile::updateMarkedState(Frame::TagNumber tagNr,
1070                                    FrameCollection& frames)
1071 {
1072   // As long as there is only a single m_marked flag, only support tag 2.
1073   if (tagNr != Frame::Tag_2)
1074     return;
1075 
1076   m_marked = false;
1077   const TagConfig& tagCfg = TagConfig::instance();
1078 
1079   if (tagCfg.markStandardViolations() &&
1080       getTagFormat(tagNr).startsWith(QLatin1String("ID3v2")) &&
1081       FrameNotice::addId3StandardViolationNotice(frames)) {
1082     m_marked = true;
1083   }
1084 
1085   if (tagCfg.markOversizedPictures()) {
1086     auto it =
1087         frames.findByExtendedType(Frame::ExtendedType(Frame::FT_Picture));
1088     while (it != frames.cend() && it->getType() == Frame::FT_Picture) {
1089       if (auto& frame = const_cast<Frame&>(*it);
1090           FrameNotice::addPictureTooLargeNotice(
1091             frame, tagCfg.maximumPictureSize())) {
1092         m_marked = true;
1093       }
1094       ++it;
1095     }
1096   }
1097 }
1098 
1099 /**
1100  * Close any file handles which are held open by the tagged file object.
1101  * The default implementation does nothing. If a concrete subclass holds
1102  * any file handles open, it has to close them in this method. This method
1103  * can be used before operations which require that a file is not open,
1104  * e.g. file renaming on Windows.
1105  */
1106 void TaggedFile::closeFileHandle()
1107 {
1108 }
1109 
1110 /**
1111  * Add a suitable field list for the frame if missing.
1112  * If a frame is created, its field list is empty. This method will create
1113  * a field list appropriate for the frame type and tagged file type if no
1114  * field list exists. The default implementation does nothing.
1115  */
1116 void TaggedFile::addFieldList(Frame::TagNumber, Frame&) const
1117 {
1118 }
1119 
1120 /**
1121  * Set frames in tag.
1122  *
1123  * @param tagNr tag number
1124  * @param frames      frame collection
1125  * @param onlyChanged only frames with value marked as changed are set
1126  */
1127 void TaggedFile::setFrames(Frame::TagNumber tagNr,
1128                            const FrameCollection& frames, bool onlyChanged)
1129 {
1130   if (tagNr == Frame::Tag_Id3v1) {
1131     for (auto it = frames.cbegin(); it != frames.cend(); ++it) {
1132       if (!onlyChanged || it->isValueChanged()) {
1133         setFrame(tagNr, *it);
1134       }
1135     }
1136   } else {
1137     bool myFramesValid = false;
1138     FrameCollection myFrames;
1139     QSet<int> replacedIndexes;
1140 
1141     for (auto it = frames.cbegin(); it != frames.cend(); ++it) {
1142       if (!onlyChanged || it->isValueChanged()) {
1143         if (it->getIndex() != -1) {
1144           // The frame has an index, so the original tag can be modified
1145           setFrame(tagNr, *it);
1146         } else {
1147           // The frame does not have an index
1148           // The frame has to be looked up and modified
1149           if (!myFramesValid) {
1150             getAllFrames(tagNr, myFrames);
1151             myFramesValid = true;
1152           }
1153           auto myIt = myFrames.find(*it);
1154           int myIndex = -1;
1155           while (myIt != myFrames.end() && !(*it < *myIt) &&
1156                  (myIndex = myIt->getIndex()) != -1) {
1157             if (!replacedIndexes.contains(myIndex)) {
1158               break;
1159             }
1160             myIndex = -1;
1161             ++myIt;
1162           }
1163           if (myIndex != -1) {
1164             replacedIndexes.insert(myIndex);
1165             if (!myIt->isFuzzyEqual(*it)) {
1166               Frame myFrame(*it);
1167               myFrame.setIndex(myIndex);
1168               setFrame(tagNr, myFrame);
1169             }
1170           } else {
1171             // Such a frame does not exist, add a new one.
1172             if (!it->getValue().isEmpty() || !it->getFieldList().isEmpty()) {
1173               Frame addedFrame(*it);
1174               addFrame(tagNr, addedFrame);
1175               Frame myFrame(*it);
1176               myFrame.setIndex(addedFrame.getIndex());
1177               setFrame(tagNr, myFrame);
1178             }
1179           }
1180         }
1181       }
1182     }
1183   }
1184 }
1185 
1186 /**
1187  * Get access and modification time of file.
1188  * @param path file path
1189  * @param actime the last access time is returned here
1190  * @param modtime the last modification time is returned here
1191  * @return true if ok.
1192  */
1193 bool TaggedFile::getFileTimeStamps(const QString& path,
1194                                    quint64& actime, quint64& modtime)
1195 {
1196 #ifdef Q_OS_WIN32
1197   int len = path.length();
1198   QVarLengthArray<wchar_t> a(len + 1);
1199   wchar_t* ws = a.data();
1200   len = path.toWCharArray(ws);
1201   ws[len] = 0;
1202   struct _stat fileStat;
1203   if (::_wstat(ws, &fileStat) == 0) {
1204     actime  = fileStat.st_atime;
1205     modtime = fileStat.st_mtime;
1206     return true;
1207   }
1208 #else
1209   struct stat fileStat;
1210   if (::stat(QFile::encodeName(path), &fileStat) == 0) {
1211     actime  = fileStat.st_atime;
1212     modtime = fileStat.st_mtime;
1213     return true;
1214   }
1215 #endif
1216   return false;
1217 }
1218 
1219 /**
1220  * Set access and modification time of file.
1221  * @param path file path
1222  * @param actime last access time
1223  * @param modtime last modification time
1224  * @return true if ok.
1225  */
1226 bool TaggedFile::setFileTimeStamps(const QString& path,
1227                                    quint64 actime, quint64 modtime)
1228 {
1229 #ifdef Q_OS_WIN32
1230   int len = path.length();
1231   QVarLengthArray<wchar_t> a(len + 1);
1232   wchar_t* ws = a.data();
1233   len = path.toWCharArray(ws);
1234   ws[len] = 0;
1235   struct _utimbuf times;
1236   times.actime = actime;
1237   times.modtime = modtime;
1238   return ::_wutime(ws, &times) == 0;
1239 #else
1240   struct utimbuf times;
1241   times.actime = actime;
1242   times.modtime = modtime;
1243   return ::utime(QFile::encodeName(path), &times) == 0;
1244 #endif
1245 }
1246 
1247 
1248 /**
1249  * Constructor.
1250  */
1251 TaggedFile::DetailInfo::DetailInfo()
1252   : channelMode(CM_None), channels(0), sampleRate(0), bitrate(0), duration(0),
1253     valid(false), vbr(false)
1254 {
1255 }
1256 
1257 /**
1258  * Get string representation of detail information.
1259  * @return information summary as string.
1260  */
1261 QString TaggedFile::DetailInfo::toString() const
1262 {
1263   QString str;
1264   if (valid) {
1265     str = format;
1266     str += QLatin1Char(' ');
1267     if (bitrate > 0 && bitrate < 16384) {
1268       if (vbr) str += QLatin1String("VBR ");
1269       str += QString::number(bitrate);
1270       str += QLatin1String(" kbps ");
1271     }
1272     if (sampleRate > 0) {
1273       str += QString::number(sampleRate);
1274       str += QLatin1String(" Hz ");
1275     }
1276     switch (channelMode) {
1277       case TaggedFile::DetailInfo::CM_Stereo:
1278         str += QLatin1String("Stereo ");
1279         break;
1280       case TaggedFile::DetailInfo::CM_JointStereo:
1281         str += QLatin1String("Joint Stereo ");
1282         break;
1283       default:
1284         if (channels > 0) {
1285           str += QString::number(channels);
1286           str += QLatin1String(" Channels ");
1287         }
1288     }
1289     if (duration > 0) {
1290       str += TaggedFile::formatTime(duration);
1291     }
1292   }
1293   return str;
1294 }