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 }