File indexing completed on 2024-05-19 04:56:31
0001 /** 0002 * \file oggfile.cpp 0003 * Handling of Ogg files. 0004 * 0005 * \b Project: Kid3 0006 * \author Urs Fleisch 0007 * \date 26 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 "oggfile.hpp" 0028 0029 #include <QFile> 0030 #include <QDir> 0031 #include <QByteArray> 0032 #include <cstdio> 0033 #include <cmath> 0034 #ifdef HAVE_VORBIS 0035 #include <vorbis/vorbisfile.h> 0036 #include "vcedit.h" 0037 #endif 0038 #include "pictureframe.h" 0039 #include "tagconfig.h" 0040 #include "taggedfilesystemmodel.h" 0041 0042 namespace { 0043 0044 /* 0045 * The following functions are used to access an Ogg/Vorbis file 0046 * using a QIODevice. They are used by vcedit_open_callbacks() and 0047 * ov_open_callbacks(). 0048 */ 0049 0050 /** 0051 * Read from a QIODevice using an fread() like interface. 0052 * @param ptr location to store data read 0053 * @param size size of one element in bytes 0054 * @param nmemb number of elements to read 0055 * @param stream QIODevice* to read from 0056 * @return number of elements read. 0057 */ 0058 size_t oggread(void* ptr, size_t size, size_t nmemb, void* stream) 0059 { 0060 if (!stream || !size) 0061 return 0; 0062 0063 auto iodev = static_cast<QIODevice*>(stream); 0064 qint64 len = iodev->read(static_cast<char*>(ptr), size * nmemb); 0065 return len / size; 0066 } 0067 0068 /** 0069 * Write to a QIODevice using an fwrite() like interface. 0070 * @param ptr location of data to write 0071 * @param size size of one element in bytes 0072 * @param nmemb number of elements to write 0073 * @param stream QIODevice* to write to 0074 * @return number of elements written. 0075 */ 0076 size_t oggwrite(const void* ptr, size_t size, size_t nmemb, void* stream) 0077 { 0078 if (!stream || !size) 0079 return 0; 0080 0081 auto iodev = static_cast<QIODevice*>(stream); 0082 qint64 len = iodev->write(static_cast<const char*>(ptr), size * nmemb); 0083 return len / size; 0084 } 0085 0086 /** 0087 * Seek in a QIODevice using an fseek() like interface. 0088 * @param stream QIODevice* to seek 0089 * @param offset byte position 0090 * @param whence SEEK_SET, SEEK_CUR, or SEEK_END 0091 * @return 0 if ok, -1 on error. 0092 */ 0093 int oggseek(void* stream, ogg_int64_t offset, int whence) 0094 { 0095 auto iodev = static_cast<QIODevice*>(stream); 0096 if (!iodev || iodev->isSequential()) 0097 return -1; 0098 0099 qint64 pos = offset; 0100 if (whence == SEEK_END) { 0101 pos += iodev->size(); 0102 } else if (whence == SEEK_CUR) { 0103 pos += iodev->pos(); 0104 } 0105 0106 if (iodev->seek(pos)) 0107 return 0; 0108 return -1; 0109 } 0110 0111 /** 0112 * Close QIODevice using an fclose() like interface. 0113 * @param stream QIODevice* to close 0114 * @return 0 if ok. 0115 */ 0116 int oggclose(void* stream) 0117 { 0118 if (auto iodev = static_cast<QIODevice*>(stream)) { 0119 iodev->close(); 0120 return 0; 0121 } 0122 return -1; 0123 } 0124 0125 /** 0126 * Get position in QIODevice using an ftell() like interface. 0127 * @param stream QIODevice* 0128 * @return current position, -1 on error. 0129 */ 0130 long oggtell(void* stream) 0131 { 0132 if (auto iodev = static_cast<QIODevice*>(stream)) { 0133 return iodev->pos(); 0134 } 0135 return -1; 0136 } 0137 0138 } 0139 0140 /** 0141 * Constructor. 0142 * 0143 * @param idx index in tagged file system model 0144 */ 0145 OggFile::OggFile(const QPersistentModelIndex& idx) 0146 : TaggedFile(idx), m_fileRead(false) 0147 { 0148 } 0149 0150 /** 0151 * Get key of tagged file format. 0152 * @return "OggMetadata". 0153 */ 0154 QString OggFile::taggedFileKey() const 0155 { 0156 return QLatin1String("OggMetadata"); 0157 } 0158 0159 #ifdef HAVE_VORBIS 0160 /** 0161 * Get features supported. 0162 * @return bit mask with Feature flags set. 0163 */ 0164 int OggFile::taggedFileFeatures() const 0165 { 0166 return TF_OggPictures; 0167 } 0168 0169 /** 0170 * Read tags from file. 0171 * 0172 * @param force true to force reading even if tags were already read. 0173 */ 0174 void OggFile::readTags(bool force) 0175 { 0176 bool priorIsTagInformationRead = isTagInformationRead(); 0177 if (force || !m_fileRead) { 0178 m_comments.clear(); 0179 markTagUnchanged(Frame::Tag_2); 0180 m_fileRead = true; 0181 0182 if (QString fnIn = currentFilePath(); readFileInfo(m_fileInfo, fnIn)) { 0183 QFile fpIn(fnIn); 0184 if (fpIn.open(QIODevice::ReadOnly)) { 0185 if (vcedit_state* state = ::vcedit_new_state()) { 0186 if (::vcedit_open_callbacks(state, &fpIn, oggread, oggwrite) >= 0) { 0187 if (vorbis_comment* vc = ::vcedit_comments(state)) { 0188 for (int i = 0; i < vc->comments; ++i) { 0189 QString userComment = 0190 QString::fromUtf8(vc->user_comments[i], 0191 vc->comment_lengths[i]); 0192 if (int equalPos = userComment.indexOf(QLatin1Char('=')); 0193 equalPos != -1) { 0194 QString name( 0195 userComment.left(equalPos).trimmed().toUpper()); 0196 if (QString value( 0197 userComment.mid(equalPos + 1).trimmed()); 0198 !value.isEmpty()) { 0199 m_comments.push_back(CommentField(name, value)); 0200 } 0201 } 0202 } 0203 } 0204 } 0205 ::vcedit_clear(state); 0206 } 0207 fpIn.close(); 0208 } 0209 } 0210 } 0211 0212 if (force) { 0213 setFilename(currentFilename()); 0214 } 0215 0216 notifyModelDataChanged(priorIsTagInformationRead); 0217 } 0218 0219 /** 0220 * Write tags to file and rename it if necessary. 0221 * 0222 * @param force true to force writing even if file was not changed. 0223 * @param renamed will be set to true if the file was renamed, 0224 * i.e. the file name is no longer valid, else *renamed 0225 * is left unchanged 0226 * @param preserve true to preserve file time stamps 0227 * 0228 * @return true if ok, false if the file could not be written or renamed. 0229 */ 0230 bool OggFile::writeTags(bool force, bool* renamed, bool preserve) 0231 { 0232 QString dirname = getDirname(); 0233 if (isChanged() && 0234 !QFileInfo(currentFilePath()).isWritable()) { 0235 revertChangedFilename(); 0236 return false; 0237 } 0238 0239 if (m_fileRead && (force || isTagChanged(Frame::Tag_2))) { 0240 bool writeOk = false; 0241 // we have to rename the original file and delete it afterwards 0242 const QString filename = currentFilename(); 0243 const QString newFilename = getFilename(); 0244 const QString tempFilename(filename + QLatin1String("_KID3")); 0245 setFilename(tempFilename); // getFilename() will now return tempFilename 0246 if (!renameFile()) { 0247 setFilename(newFilename); 0248 return false; 0249 } 0250 QString fnIn = dirname + QDir::separator() + tempFilename; 0251 QString fnOut = dirname + QDir::separator() + newFilename; 0252 QFile fpIn(fnIn); 0253 if (fpIn.open(QIODevice::ReadOnly)) { 0254 0255 // store time stamp if it has to be preserved 0256 quint64 actime = 0, modtime = 0; 0257 if (preserve) { 0258 getFileTimeStamps(fnIn, actime, modtime); 0259 } 0260 0261 QFile fpOut(fnOut); 0262 if (fpOut.open(QIODevice::WriteOnly)) { 0263 if (vcedit_state* state = ::vcedit_new_state()) { 0264 if (::vcedit_open_callbacks(state, &fpIn, oggread, oggwrite) >= 0) { 0265 if (vorbis_comment* vc = ::vcedit_comments(state)) { 0266 ::vorbis_comment_clear(vc); 0267 ::vorbis_comment_init(vc); 0268 auto it = m_comments.begin(); // clazy:exclude=detaching-member 0269 while (it != m_comments.end()) { 0270 QString name = fixUpTagKey(it->getName(), TT_Vorbis); 0271 if (QString value(it->getValue()); !value.isEmpty()) { 0272 ::vorbis_comment_add_tag( 0273 vc, 0274 name.toLatin1().data(), 0275 value.toUtf8().data()); 0276 ++it; 0277 } else { 0278 it = m_comments.erase(it); 0279 } 0280 } 0281 if (::vcedit_write(state, &fpOut) >= 0) { 0282 writeOk = true; 0283 } 0284 } 0285 } 0286 ::vcedit_clear(state); 0287 } 0288 fpOut.close(); 0289 } 0290 fpIn.close(); 0291 0292 // restore time stamp 0293 if (actime || modtime) { 0294 setFileTimeStamps(fnOut, actime, modtime); 0295 } 0296 } 0297 const TaggedFileSystemModel* model = getTaggedFileSystemModel(); 0298 if (!writeOk) { 0299 // restore old file 0300 if (!(model && const_cast<TaggedFileSystemModel*>(model)->remove( 0301 model->index(fnOut)))) { 0302 QDir(dirname).remove(newFilename); 0303 } 0304 markFilenameUnchanged(); // currentFilename() will now return tempFilename 0305 setFilename(newFilename); // getFilename() will now return newFilename 0306 renameFile(); 0307 markFilenameUnchanged(); // currentFilename() will now return newFilename 0308 return false; 0309 } 0310 markTagUnchanged(Frame::Tag_2); 0311 if (!(model && const_cast<TaggedFileSystemModel*>(model)->remove( 0312 model->index(fnIn)))) { 0313 QDir(dirname).remove(tempFilename); 0314 } 0315 setFilename(newFilename); 0316 if (isFilenameChanged()) { 0317 markFilenameUnchanged(); 0318 *renamed = true; 0319 } 0320 } else if (isFilenameChanged()) { 0321 // tags not changed, but file name 0322 if (!renameFile()) { 0323 return false; 0324 } 0325 markFilenameUnchanged(); 0326 *renamed = true; 0327 } 0328 return true; 0329 } 0330 0331 /** 0332 * Free resources allocated when calling readTags(). 0333 * 0334 * @param force true to force clearing even if the tags are modified 0335 */ 0336 void OggFile::clearTags(bool force) 0337 { 0338 if (!m_fileRead || (isChanged() && !force)) 0339 return; 0340 0341 bool priorIsTagInformationRead = isTagInformationRead(); 0342 m_comments.clear(); 0343 markTagUnchanged(Frame::Tag_2); 0344 m_fileRead = false; 0345 notifyModelDataChanged(priorIsTagInformationRead); 0346 } 0347 #else // HAVE_VORBIS 0348 void OggFile::readTags(bool) {} 0349 bool OggFile::writeTags(bool, bool*, bool) { return false; } 0350 void OggFile::clearTags(bool) {} 0351 #endif // HAVE_VORBIS 0352 0353 namespace { 0354 0355 /** 0356 * Get name of frame from type. 0357 * 0358 * @param type type 0359 * 0360 * @return name. 0361 */ 0362 const char* getVorbisNameFromType(Frame::Type type) 0363 { 0364 static const char* const names[] = { 0365 "TITLE", // FT_Title, 0366 "ARTIST", // FT_Artist, 0367 "ALBUM", // FT_Album, 0368 "COMMENT", // FT_Comment, 0369 "DATE", // FT_Date, 0370 "TRACKNUMBER", // FT_Track, 0371 "GENRE", // FT_Genre, 0372 // FT_LastV1Frame = FT_Track, 0373 "ALBUMARTIST", // FT_AlbumArtist, 0374 "ARRANGER", // FT_Arranger, 0375 "AUTHOR", // FT_Author, 0376 "BPM", // FT_Bpm, 0377 "CATALOGNUMBER", // FT_CatalogNumber, 0378 "COMPILATION", // FT_Compilation, 0379 "COMPOSER", // FT_Composer, 0380 "CONDUCTOR", // FT_Conductor, 0381 "COPYRIGHT", // FT_Copyright, 0382 "DISCNUMBER", // FT_Disc, 0383 "ENCODED-BY", // FT_EncodedBy, 0384 "ENCODERSETTINGS", // FT_EncoderSettings, 0385 "ENCODINGTIME", // FT_EncodingTime, 0386 "GROUPING", // FT_Grouping, 0387 "INITIALKEY", // FT_InitialKey, 0388 "ISRC", // FT_Isrc, 0389 "LANGUAGE", // FT_Language, 0390 "LYRICIST", // FT_Lyricist, 0391 "LYRICS", // FT_Lyrics, 0392 "SOURCEMEDIA", // FT_Media, 0393 "MOOD", // FT_Mood, 0394 "ORIGINALALBUM", // FT_OriginalAlbum, 0395 "ORIGINALARTIST", // FT_OriginalArtist, 0396 "ORIGINALDATE", // FT_OriginalDate, 0397 "DESCRIPTION", // FT_Description, 0398 "PERFORMER", // FT_Performer, 0399 "METADATA_BLOCK_PICTURE", // FT_Picture, 0400 "PUBLISHER", // FT_Publisher, 0401 "RELEASECOUNTRY", // FT_ReleaseCountry, 0402 "REMIXER", // FT_Remixer, 0403 "ALBUMSORT", // FT_SortAlbum, 0404 "ALBUMARTISTSORT", // FT_SortAlbumArtist, 0405 "ARTISTSORT", // FT_SortArtist, 0406 "COMPOSERSORT", // FT_SortComposer, 0407 "TITLESORT", // FT_SortName, 0408 "SUBTITLE", // FT_Subtitle, 0409 "WEBSITE", // FT_Website, 0410 "WWWAUDIOFILE", // FT_WWWAudioFile, 0411 "WWWAUDIOSOURCE", // FT_WWWAudioSource, 0412 "RELEASEDATE", // FT_ReleaseDate, 0413 "RATING", // FT_Rating, 0414 "WORK" // FT_Work, 0415 // FT_Custom1 0416 }; 0417 Q_STATIC_ASSERT(std::size(names) == Frame::FT_Custom1); 0418 if (type == Frame::FT_Picture && 0419 TagConfig::instance().pictureNameIndex() == TagConfig::VP_COVERART) { 0420 return "COVERART"; 0421 } 0422 if (Frame::isCustomFrameType(type)) { 0423 return Frame::getNameForCustomFrame(type); 0424 } 0425 return type <= Frame::FT_LastFrame ? names[type] : "UNKNOWN"; 0426 } 0427 0428 /** 0429 * Get the frame type for a Vorbis name. 0430 * 0431 * @param name Vorbis tag name 0432 * 0433 * @return frame type. 0434 */ 0435 Frame::Type getTypeFromVorbisName(QString name) 0436 { 0437 static QMap<QString, int> strNumMap; 0438 if (strNumMap.empty()) { 0439 // first time initialization 0440 for (int i = 0; i < Frame::FT_Custom1; ++i) { 0441 auto type = static_cast<Frame::Type>(i); 0442 strNumMap.insert(QString::fromLatin1(getVorbisNameFromType(type)), type); 0443 } 0444 strNumMap.insert(QLatin1String("COVERART"), Frame::FT_Picture); 0445 strNumMap.insert(QLatin1String("METADATA_BLOCK_PICTURE"), Frame::FT_Picture); 0446 } 0447 if (auto it = strNumMap.constFind(name.remove(QLatin1Char('=')).toUpper()); 0448 it != strNumMap.constEnd()) { 0449 return static_cast<Frame::Type>(*it); 0450 } 0451 return Frame::getTypeFromCustomFrameName(name.toLatin1()); 0452 } 0453 0454 /** 0455 * Get internal name of a Vorbis frame. 0456 * 0457 * @param frame frame 0458 * 0459 * @return Vorbis key. 0460 */ 0461 QString getVorbisName(const Frame& frame) 0462 { 0463 if (Frame::Type type = frame.getType(); type <= Frame::FT_LastFrame) { 0464 return QString::fromLatin1(getVorbisNameFromType(type)); 0465 } 0466 return frame.getName().remove(QLatin1Char('=')).toUpper(); 0467 } 0468 0469 } 0470 0471 /** 0472 * Remove frames. 0473 * 0474 * @param tagNr tag number 0475 * @param flt filter specifying which frames to remove 0476 */ 0477 void OggFile::deleteFrames(Frame::TagNumber tagNr, const FrameFilter& flt) 0478 { 0479 if (tagNr != Frame::Tag_2) 0480 return; 0481 0482 if (flt.areAllEnabled()) { 0483 m_comments.clear(); 0484 markTagChanged(Frame::Tag_2, Frame::ExtendedType()); 0485 } else { 0486 bool changed = false; 0487 for (auto it = m_comments.begin(); it != m_comments.end();) { // clazy:exclude=detaching-member 0488 if (QString name(it->getName()); 0489 flt.isEnabled(getTypeFromVorbisName(name), name)) { 0490 it = m_comments.erase(it); 0491 changed = true; 0492 } else { 0493 ++it; 0494 } 0495 } 0496 if (changed) { 0497 markTagChanged(Frame::Tag_2, Frame::ExtendedType()); 0498 } 0499 } 0500 } 0501 0502 /** 0503 * Get text field. 0504 * 0505 * @param name name 0506 * @return value, "" if not found, 0507 * QString::null if the tags have not been read yet. 0508 */ 0509 QString OggFile::getTextField(const QString& name) const 0510 { 0511 if (m_fileRead) { 0512 return m_comments.getValue(name); 0513 } 0514 return QString(); 0515 } 0516 0517 /** 0518 * Set text field. 0519 * If value is null if the tags have not been read yet, nothing is changed. 0520 * If value is different from the current value, tag 2 is marked as changed. 0521 * 0522 * @param name name 0523 * @param value value, "" to remove, QString::null to do nothing 0524 * @param type frame type 0525 */ 0526 void OggFile::setTextField(const QString& name, const QString& value, 0527 const Frame::ExtendedType& type) 0528 { 0529 if (m_fileRead && !value.isNull() && 0530 m_comments.setValue(name, value)) { 0531 markTagChanged(Frame::Tag_2, type); 0532 } 0533 } 0534 0535 /** 0536 * Check if tag information has already been read. 0537 * 0538 * @return true if information is available, 0539 * false if the tags have not been read yet, in which case 0540 * hasTag() does not return meaningful information. 0541 */ 0542 bool OggFile::isTagInformationRead() const 0543 { 0544 return m_fileRead; 0545 } 0546 0547 /** 0548 * Check if file has a tag. 0549 * 0550 * @param tagNr tag number 0551 * @return true if a tag is available. 0552 * @see isTagInformationRead() 0553 */ 0554 bool OggFile::hasTag(Frame::TagNumber tagNr) const 0555 { 0556 return tagNr == Frame::Tag_2 && !m_comments.empty(); 0557 } 0558 0559 /** 0560 * Get file extension including the dot. 0561 * 0562 * @return file extension ".ogg". 0563 */ 0564 QString OggFile::getFileExtension() const 0565 { 0566 return QLatin1String(".ogg"); 0567 } 0568 0569 #ifdef HAVE_VORBIS 0570 /** 0571 * Get technical detail information. 0572 * 0573 * @param info the detail information is returned here 0574 */ 0575 void OggFile::getDetailInfo(DetailInfo& info) const 0576 { 0577 if (m_fileRead && m_fileInfo.valid) { 0578 info.valid = true; 0579 info.format = QLatin1String("Ogg Vorbis"); 0580 info.bitrate = m_fileInfo.bitrate / 1000; 0581 info.sampleRate = m_fileInfo.sampleRate; 0582 info.channels = m_fileInfo.channels; 0583 info.duration = m_fileInfo.duration; 0584 } else { 0585 info.valid = false; 0586 } 0587 } 0588 0589 /** 0590 * Get duration of file. 0591 * 0592 * @return duration in seconds, 0593 * 0 if unknown. 0594 */ 0595 unsigned OggFile::getDuration() const 0596 { 0597 if (m_fileRead && m_fileInfo.valid) { 0598 return m_fileInfo.duration; 0599 } 0600 return 0; 0601 } 0602 #else // HAVE_VORBIS 0603 void OggFile::getDetailInfo(DetailInfo& info) const { info.valid = false; } 0604 unsigned OggFile::getDuration() const { return 0; } 0605 #endif // HAVE_VORBIS 0606 0607 /** 0608 * Get the format of tag. 0609 * 0610 * @param tagNr tag number 0611 * @return "Vorbis". 0612 */ 0613 QString OggFile::getTagFormat(Frame::TagNumber tagNr) const 0614 { 0615 return hasTag(tagNr) ? QLatin1String("Vorbis") : QString(); 0616 } 0617 0618 /** 0619 * Get a specific frame from the tags. 0620 * 0621 * @param tagNr tag number 0622 * @param type frame type 0623 * @param frame the frame is returned here 0624 * 0625 * @return true if ok. 0626 */ 0627 bool OggFile::getFrame(Frame::TagNumber tagNr, Frame::Type type, Frame& frame) const 0628 { 0629 if (type < Frame::FT_FirstFrame || type > Frame::FT_LastV1Frame || 0630 tagNr > 1) 0631 return false; 0632 0633 if (tagNr == Frame::Tag_1) { 0634 frame.setValue(QString()); 0635 } else { 0636 frame.setValue(getTextField( 0637 QString::fromLatin1(getVorbisNameFromType(type)))); 0638 } 0639 frame.setType(type); 0640 return true; 0641 } 0642 0643 /** 0644 * Set a frame in the tags. 0645 * 0646 * @param tagNr tag number 0647 * @param frame frame to set 0648 * 0649 * @return true if ok. 0650 */ 0651 bool OggFile::setFrame(Frame::TagNumber tagNr, const Frame& frame) 0652 { 0653 if (tagNr == Frame::Tag_2) { 0654 if (frame.getType() == Frame::FT_Track) { 0655 if (int numTracks = getTotalNumberOfTracksIfEnabled(); numTracks > 0) { 0656 QString numTracksStr = QString::number(numTracks); 0657 formatTrackNumberIfEnabled(numTracksStr, false); 0658 if (const QString trackTotalName(QLatin1String("TRACKTOTAL")); 0659 getTextField(trackTotalName) != numTracksStr) { 0660 Frame::ExtendedType extendedType(Frame::FT_Other, trackTotalName); 0661 setTextField(trackTotalName, numTracksStr, extendedType); 0662 markTagChanged(Frame::Tag_2, extendedType); 0663 } 0664 } 0665 } 0666 0667 // If the frame has an index, change that specific frame 0668 if (int index = frame.getIndex(); 0669 index >= 0 && index < m_comments.size()) { 0670 QString value = frame.getValue(); 0671 if (frame.getType() == Frame::FT_Picture) { 0672 Frame newFrame(frame); 0673 PictureFrame::setDescription(newFrame, value); 0674 PictureFrame::getFieldsToBase64(newFrame, value); 0675 if (!value.isEmpty() && frame.getInternalName() == QLatin1String("COVERART")) { 0676 QString mimeType; 0677 PictureFrame::getMimeType(frame, mimeType); 0678 const QString coverArtMimeName(QLatin1String("COVERARTMIME")); 0679 setTextField(coverArtMimeName, mimeType, 0680 Frame::ExtendedType(Frame::FT_Other, coverArtMimeName)); 0681 } 0682 } else if (frame.getType() == Frame::FT_Track) { 0683 formatTrackNumberIfEnabled(value, false); 0684 } 0685 if (m_comments[index].getValue() != value) { 0686 m_comments[index].setValue(value); 0687 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 0688 } 0689 return true; 0690 } 0691 } 0692 0693 // Try the basic method 0694 Frame::Type type = frame.getType(); 0695 if (type < Frame::FT_FirstFrame || type > Frame::FT_LastV1Frame || 0696 tagNr > 1) 0697 return false; 0698 0699 if (tagNr == Frame::Tag_2) { 0700 if (type == Frame::FT_Track) { 0701 int numTracks; 0702 if (int num = splitNumberAndTotal(frame.getValue(), &numTracks); 0703 num >= 0) { 0704 QString str; 0705 if (num != 0) { 0706 str.setNum(num); 0707 formatTrackNumberIfEnabled(str, false); 0708 } else { 0709 str = QLatin1String(""); 0710 } 0711 const QString trackNumberName(QLatin1String("TRACKNUMBER")); 0712 setTextField(trackNumberName, str, 0713 Frame::ExtendedType(Frame::FT_Track, trackNumberName)); 0714 if (numTracks > 0) { 0715 str.setNum(numTracks); 0716 formatTrackNumberIfEnabled(str, false); 0717 const QString trackTotalName(QLatin1String("TRACKTOTAL")); 0718 setTextField(trackTotalName, str, 0719 Frame::ExtendedType(Frame::FT_Other, trackTotalName)); 0720 } 0721 } 0722 } else { 0723 const QString fieldName = type == Frame::FT_Comment 0724 ? getCommentFieldName() 0725 : QString::fromLatin1(getVorbisNameFromType(type)); 0726 setTextField(fieldName, 0727 frame.getValue(), Frame::ExtendedType(type, fieldName)); 0728 } 0729 } 0730 return true; 0731 } 0732 0733 /** 0734 * Add a frame in the tags. 0735 * 0736 * @param tagNr tag number 0737 * @param frame frame to add 0738 * 0739 * @return true if ok. 0740 */ 0741 bool OggFile::addFrame(Frame::TagNumber tagNr, Frame& frame) 0742 { 0743 if (tagNr == Frame::Tag_2) { 0744 // Add a new frame. 0745 QString name(getVorbisName(frame)); 0746 QString value(frame.getValue()); 0747 if (frame.getType() == Frame::FT_Picture) { 0748 if (frame.getFieldList().empty()) { 0749 PictureFrame::setFields( 0750 frame, Frame::TE_ISO8859_1, QLatin1String(""), QLatin1String("image/jpeg"), 0751 PictureFrame::PT_CoverFront, QLatin1String(""), QByteArray()); 0752 } 0753 frame.setExtendedType(Frame::ExtendedType(Frame::FT_Picture, name)); 0754 PictureFrame::getFieldsToBase64(frame, value); 0755 } 0756 m_comments.push_back(OggFile::CommentField(name, value)); 0757 frame.setExtendedType(Frame::ExtendedType(frame.getType(), name)); 0758 frame.setIndex(m_comments.size() - 1); 0759 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 0760 return true; 0761 } 0762 return false; 0763 } 0764 0765 /** 0766 * Delete a frame in the tags. 0767 * 0768 * @param tagNr tag number 0769 * @param frame frame to delete. 0770 * 0771 * @return true if ok. 0772 */ 0773 bool OggFile::deleteFrame(Frame::TagNumber tagNr, const Frame& frame) 0774 { 0775 if (tagNr == Frame::Tag_2) { 0776 // If the frame has an index, delete that specific frame 0777 if (int index = frame.getIndex(); 0778 index >= 0 && index < m_comments.size()) { 0779 m_comments.removeAt(index); 0780 markTagChanged(Frame::Tag_2, frame.getExtendedType()); 0781 return true; 0782 } 0783 } 0784 0785 // Try the superclass method 0786 return TaggedFile::deleteFrame(tagNr, frame); 0787 } 0788 0789 /** 0790 * Get all frames in tag. 0791 * 0792 * @param tagNr tag number 0793 * @param frames frame collection to set. 0794 */ 0795 void OggFile::getAllFrames(Frame::TagNumber tagNr, FrameCollection& frames) 0796 { 0797 if (tagNr == Frame::Tag_2) { 0798 frames.clear(); 0799 int i = 0; 0800 for (auto it = m_comments.constBegin(); it != m_comments.constEnd(); ++it) { 0801 QString name = it->getName(); 0802 if (Frame::Type type = getTypeFromVorbisName(name); 0803 type == Frame::FT_Picture) { 0804 Frame frame(type, QLatin1String(""), name, i++); 0805 PictureFrame::setFieldsFromBase64(frame, it->getValue()); 0806 if (name == QLatin1String("COVERART")) { 0807 PictureFrame::setMimeType(frame, getTextField(QLatin1String("COVERARTMIME"))); 0808 } 0809 frames.insert(frame); 0810 } else { 0811 frames.insert(Frame(type, it->getValue(), name, i++)); 0812 } 0813 } 0814 updateMarkedState(tagNr, frames); 0815 frames.addMissingStandardFrames(); 0816 return; 0817 } 0818 0819 TaggedFile::getAllFrames(tagNr, frames); 0820 } 0821 0822 /** 0823 * Get a list of frame IDs which can be added. 0824 * @param tagNr tag number 0825 * @return list with frame IDs. 0826 */ 0827 QStringList OggFile::getFrameIds(Frame::TagNumber tagNr) const 0828 { 0829 if (tagNr != Frame::Tag_2) 0830 return QStringList(); 0831 0832 static const char* const fieldNames[] = { 0833 "CONTACT", 0834 "DISCTOTAL", 0835 "EAN/UPN", 0836 "ENCODING", 0837 "ENGINEER", 0838 "ENSEMBLE", 0839 "GUESTARTIST", 0840 "LABEL", 0841 "LABELNO", 0842 "LICENSE", 0843 "LOCATION", 0844 "OPUS", 0845 "ORGANIZATION", 0846 "PARTNUMBER", 0847 "PRODUCER", 0848 "PRODUCTNUMBER", 0849 "RECORDINGDATE", 0850 "TRACKTOTAL", 0851 "VERSION", 0852 "VOLUME" 0853 }; 0854 0855 QStringList lst; 0856 lst.reserve(Frame::FT_LastFrame - Frame::FT_FirstFrame + 1 + 0857 std::size(fieldNames)); 0858 for (int k = Frame::FT_FirstFrame; k <= Frame::FT_LastFrame; ++k) { 0859 if (auto name = Frame::ExtendedType(static_cast<Frame::Type>(k), 0860 QLatin1String("")).getName(); 0861 !name.isEmpty()) { 0862 lst.append(name); 0863 } 0864 } 0865 for (auto fieldName : fieldNames) { 0866 lst.append(QString::fromLatin1(fieldName)); 0867 } 0868 return lst; 0869 } 0870 0871 #ifdef HAVE_VORBIS 0872 /** 0873 * Read information about an Ogg/Vorbis file. 0874 * @param info file info to fill 0875 * @param fn file name 0876 * @return true if ok. 0877 */ 0878 bool OggFile::readFileInfo(FileInfo& info, const QString& fn) const 0879 { 0880 static ::ov_callbacks ovcb = { 0881 oggread, oggseek, oggclose, oggtell 0882 }; 0883 info.valid = false; 0884 QFile fp(fn); 0885 if (fp.open(QIODevice::ReadOnly)) { 0886 OggVorbis_File vf; 0887 if (::ov_open_callbacks(&fp, &vf, nullptr, 0, ovcb) == 0) { 0888 if (vorbis_info* vi = ::ov_info(&vf, -1)) { 0889 info.valid = true; 0890 info.version = vi->version; 0891 info.channels = vi->channels; 0892 info.sampleRate = vi->rate; 0893 info.bitrate = vi->bitrate_nominal; 0894 if (info.bitrate <= 0) { 0895 info.bitrate = vi->bitrate_upper; 0896 } 0897 if (info.bitrate <= 0) { 0898 info.bitrate = vi->bitrate_lower; 0899 } 0900 } 0901 info.duration = static_cast<long>(::ov_time_total(&vf, -1)); 0902 ::ov_clear(&vf); // closes file, do not use ::fclose() 0903 } else { 0904 fp.close(); 0905 } 0906 } 0907 return info.valid; 0908 } 0909 #endif // HAVE_VORBIS 0910 0911 /** 0912 * Get value. 0913 * @param name name 0914 * @return value, "" if not found. 0915 */ 0916 QString OggFile::CommentList::getValue(const QString& name) const 0917 { 0918 for (const_iterator it = begin(); it != end(); ++it) { 0919 if (it->getName() == name) { 0920 return it->getValue(); 0921 } 0922 } 0923 return QLatin1String(""); 0924 } 0925 0926 /** 0927 * Set value. 0928 * @param name name 0929 * @param value value 0930 * @return true if value was changed. 0931 */ 0932 bool OggFile::CommentList::setValue(const QString& name, const QString& value) 0933 { 0934 for (iterator it = begin(); it != end(); ++it) { 0935 if (it->getName() == name) { 0936 if (QString oldValue = it->getValue(); value != oldValue) { 0937 it->setValue(value); 0938 return true; 0939 } 0940 return false; 0941 } 0942 } 0943 if (!value.isEmpty()) { 0944 CommentField cf(name, value); 0945 push_back(cf); 0946 return true; 0947 } 0948 return false; 0949 }