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, ×) == 0; 1239 #else 1240 struct utimbuf times; 1241 times.actime = actime; 1242 times.modtime = modtime; 1243 return ::utime(QFile::encodeName(path), ×) == 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 }