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

0001 /**
0002  * \file frame.cpp
0003  * Generalized frame.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 25 Aug 2007
0008  *
0009  * Copyright (C) 2007-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 "frame.h"
0028 #include <QVector>
0029 #include <QMap>
0030 #include <QStringList>
0031 #include <QRegularExpression>
0032 #include <QCoreApplication>
0033 #include <QFile>
0034 #include <QTextStream>
0035 #if QT_VERSION >= 0x060000
0036 #include <QStringConverter>
0037 #else
0038 #include <QTextCodec>
0039 #endif
0040 #include "pictureframe.h"
0041 
0042 namespace {
0043 
0044 const char* const fieldIdNames[] = {
0045   "Unknown",
0046   QT_TRANSLATE_NOOP("@default", "Text Encoding"),
0047   QT_TRANSLATE_NOOP("@default", "Text"),
0048   QT_TRANSLATE_NOOP("@default", "URL"),
0049   QT_TRANSLATE_NOOP("@default", "Data"),
0050   QT_TRANSLATE_NOOP("@default", "Description"),
0051   QT_TRANSLATE_NOOP("@default", "Owner"),
0052   QT_TRANSLATE_NOOP("@default", "Email"),
0053   QT_TRANSLATE_NOOP("@default", "Rating"),
0054   QT_TRANSLATE_NOOP("@default", "Filename"),
0055   QT_TRANSLATE_NOOP("@default", "Language"),
0056   QT_TRANSLATE_NOOP("@default", "Picture Type"),
0057   QT_TRANSLATE_NOOP("@default", "Image format"),
0058   QT_TRANSLATE_NOOP("@default", "Mimetype"),
0059   QT_TRANSLATE_NOOP("@default", "Counter"),
0060   QT_TRANSLATE_NOOP("@default", "Identifier"),
0061   QT_TRANSLATE_NOOP("@default", "Volume Adjustment"),
0062   QT_TRANSLATE_NOOP("@default", "Number of Bits"),
0063   QT_TRANSLATE_NOOP("@default", "Volume Change Right"),
0064   QT_TRANSLATE_NOOP("@default", "Volume Change Left"),
0065   QT_TRANSLATE_NOOP("@default", "Peak Volume Right"),
0066   QT_TRANSLATE_NOOP("@default", "Peak Volume Left"),
0067   QT_TRANSLATE_NOOP("@default", "Timestamp Format"),
0068   QT_TRANSLATE_NOOP("@default", "Content Type"),
0069 
0070   QT_TRANSLATE_NOOP("@default", "Price"),
0071   QT_TRANSLATE_NOOP("@default", "Date"),
0072   QT_TRANSLATE_NOOP("@default", "Seller"),
0073   nullptr
0074 };
0075 
0076 const char* const textEncodingNames[] = {
0077   QT_TRANSLATE_NOOP("@default", "ISO-8859-1"),
0078   QT_TRANSLATE_NOOP("@default", "UTF16"),
0079   QT_TRANSLATE_NOOP("@default", "UTF16BE"),
0080   QT_TRANSLATE_NOOP("@default", "UTF8"),
0081   nullptr
0082 };
0083 
0084 const char* const timestampFormatNames[] = {
0085   QT_TRANSLATE_NOOP("@default", "Other"),
0086   QT_TRANSLATE_NOOP("@default", "MPEG frames as unit"),
0087   QT_TRANSLATE_NOOP("@default", "Milliseconds as unit"),
0088   nullptr
0089 };
0090 
0091 const char* const contentTypeNames[] = {
0092   QT_TRANSLATE_NOOP("@default", "Other"),
0093   QT_TRANSLATE_NOOP("@default", "Lyrics"),
0094   QT_TRANSLATE_NOOP("@default", "Text transcription"),
0095   QT_TRANSLATE_NOOP("@default", "Movement/part name"),
0096   QT_TRANSLATE_NOOP("@default", "Events"),
0097   QT_TRANSLATE_NOOP("@default", "Chord"),
0098   QT_TRANSLATE_NOOP("@default", "Trivia/pop up"),
0099   nullptr
0100 };
0101 
0102 // Custom frame names.
0103 QVector<QByteArray> customFrameNames(Frame::NUM_CUSTOM_FRAME_NAMES);
0104 
0105 // Map of custom frame names to frame type.
0106 QMap<QByteArray, int> customFrameNameMap;
0107 
0108 /**
0109  * Get name of frame from type.
0110  *
0111  * @param type type
0112  *
0113  * @return name.
0114  */
0115 const char* getNameFromType(Frame::Type type)
0116 {
0117   static const char* const names[] = {
0118     QT_TRANSLATE_NOOP("@default", "Title"),           // FT_Title,
0119     QT_TRANSLATE_NOOP("@default", "Artist"),          // FT_Artist,
0120     QT_TRANSLATE_NOOP("@default", "Album"),           // FT_Album,
0121     QT_TRANSLATE_NOOP("@default", "Comment"),         // FT_Comment,
0122     QT_TRANSLATE_NOOP("@default", "Date"),            // FT_Date,
0123     QT_TRANSLATE_NOOP("@default", "Track Number"),    // FT_Track,
0124     QT_TRANSLATE_NOOP("@default", "Genre"),           // FT_Genre,
0125                                   // FT_LastV1Frame = FT_Track,
0126     QT_TRANSLATE_NOOP("@default", "Album Artist"),    // FT_AlbumArtist
0127     QT_TRANSLATE_NOOP("@default", "Arranger"),        // FT_Arranger,
0128     QT_TRANSLATE_NOOP("@default", "Author"),          // FT_Author,
0129     QT_TRANSLATE_NOOP("@default", "BPM"),             // FT_Bpm,
0130     QT_TRANSLATE_NOOP("@default", "Catalog Number"),  // FT_CatalogNumber,
0131     QT_TRANSLATE_NOOP("@default", "Compilation"),     // FT_Compilation,
0132     QT_TRANSLATE_NOOP("@default", "Composer"),        // FT_Composer,
0133     QT_TRANSLATE_NOOP("@default", "Conductor"),       // FT_Conductor,
0134     QT_TRANSLATE_NOOP("@default", "Copyright"),       // FT_Copyright,
0135     QT_TRANSLATE_NOOP("@default", "Disc Number"),     // FT_Disc,
0136     QT_TRANSLATE_NOOP("@default", "Encoded-by"),      // FT_EncodedBy,
0137     QT_TRANSLATE_NOOP("@default", "Encoder Settings"), // FT_EncoderSettings,
0138     QT_TRANSLATE_NOOP("@default", "Encoding Time"),   // FT_EncodingTime,
0139     QT_TRANSLATE_NOOP("@default", "Grouping"),        // FT_Grouping,
0140     QT_TRANSLATE_NOOP("@default", "Initial Key"),     // FT_InitialKey,
0141     QT_TRANSLATE_NOOP("@default", "ISRC"),            // FT_Isrc,
0142     QT_TRANSLATE_NOOP("@default", "Language"),        // FT_Language,
0143     QT_TRANSLATE_NOOP("@default", "Lyricist"),        // FT_Lyricist,
0144     QT_TRANSLATE_NOOP("@default", "Lyrics"),          // FT_Lyrics,
0145     QT_TRANSLATE_NOOP("@default", "Media"),           // FT_Media,
0146     QT_TRANSLATE_NOOP("@default", "Mood"),            // FT_Mood,
0147     QT_TRANSLATE_NOOP("@default", "Original Album"),  // FT_OriginalAlbum,
0148     QT_TRANSLATE_NOOP("@default", "Original Artist"), // FT_OriginalArtist,
0149     QT_TRANSLATE_NOOP("@default", "Original Date"),   // FT_OriginalDate,
0150     QT_TRANSLATE_NOOP("@default", "Description"),     // FT_Description,
0151     QT_TRANSLATE_NOOP("@default", "Performer"),       // FT_Performer,
0152     QT_TRANSLATE_NOOP("@default", "Picture"),         // FT_Picture,
0153     QT_TRANSLATE_NOOP("@default", "Publisher"),       // FT_Publisher,
0154     QT_TRANSLATE_NOOP("@default", "Release Country"), // FT_ReleaseCountry,
0155     QT_TRANSLATE_NOOP("@default", "Remixer"),         // FT_Remixer,
0156     QT_TRANSLATE_NOOP("@default", "Sort Album"),      // FT_SortAlbum,
0157     QT_TRANSLATE_NOOP("@default", "Sort Album Artist"), // FT_SortAlbumArtist,
0158     QT_TRANSLATE_NOOP("@default", "Sort Artist"),     // FT_SortArtist,
0159     QT_TRANSLATE_NOOP("@default", "Sort Composer"),   // FT_SortComposer,
0160     QT_TRANSLATE_NOOP("@default", "Sort Name"),       // FT_SortName,
0161     QT_TRANSLATE_NOOP("@default", "Subtitle"),        // FT_Subtitle,
0162     QT_TRANSLATE_NOOP("@default", "Website"),         // FT_Website,
0163     QT_TRANSLATE_NOOP("@default", "WWW Audio File"),  // FT_WWWAudioFile,
0164     QT_TRANSLATE_NOOP("@default", "WWW Audio Source"), // FT_WWWAudioSource,
0165     QT_TRANSLATE_NOOP("@default", "Release Date"),    // FT_ReleaseDate,
0166     QT_TRANSLATE_NOOP("@default", "Rating"),          // FT_Rating,
0167     QT_TRANSLATE_NOOP("@default", "Work")             // FT_Work,
0168                                                       // FT_Custom1
0169   };
0170   Q_STATIC_ASSERT(std::size(names) == Frame::FT_Custom1);
0171   if (Frame::isCustomFrameType(type)) {
0172     return Frame::getNameForCustomFrame(type).constData();
0173   }
0174   return type < Frame::FT_Custom1 ? names[type] : "Unknown";
0175 }
0176 
0177 /**
0178  * Get map of non unified frame names to display names.
0179  * @return mapping of frame names to display names.
0180  */
0181 QMap<QByteArray, QByteArray> getDisplayNamesOfIds()
0182 {
0183   static const struct StrOfId {
0184     const char* id;
0185     const char* str;
0186   } strOfId[] = {
0187     { "AENC", QT_TRANSLATE_NOOP("@default", "Audio Encryption") },
0188     { "ASPI", QT_TRANSLATE_NOOP("@default", "Audio Seek Point") },
0189     { "CHAP", QT_TRANSLATE_NOOP("@default", "Chapter") },
0190     { "COMR", QT_TRANSLATE_NOOP("@default", "Commercial") },
0191     { "CTOC", QT_TRANSLATE_NOOP("@default", "Table of Contents") },
0192     { "ENCR", QT_TRANSLATE_NOOP("@default", "Encryption Method") },
0193     { "EQU2", QT_TRANSLATE_NOOP("@default", "Equalization") },
0194     { "EQUA", QT_TRANSLATE_NOOP("@default", "Equalization") },
0195     { "ETCO", QT_TRANSLATE_NOOP("@default", "Event Timing Codes") },
0196     { "GEOB", QT_TRANSLATE_NOOP("@default", "General Object") },
0197     { "GRID", QT_TRANSLATE_NOOP("@default", "Group Identification") },
0198     { "GRP1", QT_TRANSLATE_NOOP("@default", "Grouping") },
0199     { "LINK", QT_TRANSLATE_NOOP("@default", "Linked Information") },
0200     { "MCDI", QT_TRANSLATE_NOOP("@default", "Music CD Identifier") },
0201     { "MLLT", QT_TRANSLATE_NOOP("@default", "MPEG Lookup Table") },
0202     { "MVIN", QT_TRANSLATE_NOOP("@default", "Movement Number") },
0203     { "MVNM", QT_TRANSLATE_NOOP("@default", "Movement Name") },
0204     { "OWNE", QT_TRANSLATE_NOOP("@default", "Ownership") },
0205     { "PCNT", QT_TRANSLATE_NOOP("@default", "Play Counter") },
0206     { "PCST", QT_TRANSLATE_NOOP("@default", "Podcast") },
0207     { "POPM", QT_TRANSLATE_NOOP("@default", "Popularimeter") },
0208     { "POSS", QT_TRANSLATE_NOOP("@default", "Position Synchronisation") },
0209     { "PRIV", QT_TRANSLATE_NOOP("@default", "Private") },
0210     { "RBUF", QT_TRANSLATE_NOOP("@default", "Recommended Buffer Size") },
0211     { "RVA2", QT_TRANSLATE_NOOP("@default", "Volume Adjustment") },
0212     { "RVAD", QT_TRANSLATE_NOOP("@default", "Volume Adjustment") },
0213     { "RVRB", QT_TRANSLATE_NOOP("@default", "Reverb") },
0214     { "SEEK", QT_TRANSLATE_NOOP("@default", "Seek") },
0215     { "SIGN", QT_TRANSLATE_NOOP("@default", "Signature") },
0216     { "SYLT", QT_TRANSLATE_NOOP("@default", "Synchronized Lyrics") },
0217     { "SYTC", QT_TRANSLATE_NOOP("@default", "Synchronized Tempo Codes") },
0218     { "TCAT", QT_TRANSLATE_NOOP("@default", "Podcast Category") },
0219     { "TDAT", QT_TRANSLATE_NOOP("@default", "Date") },
0220     { "TDEN", QT_TRANSLATE_NOOP("@default", "Encoding Time") },
0221     { "TDES", QT_TRANSLATE_NOOP("@default", "Podcast Description") },
0222     { "TDLY", QT_TRANSLATE_NOOP("@default", "Playlist Delay") },
0223     { "TDOR", QT_TRANSLATE_NOOP("@default", "Original Release Time") },
0224     { "TDRC", QT_TRANSLATE_NOOP("@default", "Recording Time") },
0225     { "TDRL", QT_TRANSLATE_NOOP("@default", "Release Time") },
0226     { "TDTG", QT_TRANSLATE_NOOP("@default", "Tagging Time") },
0227     { "TFLT", QT_TRANSLATE_NOOP("@default", "File Type") },
0228     { "TGID", QT_TRANSLATE_NOOP("@default", "Podcast Identifier") },
0229     { "TIME", QT_TRANSLATE_NOOP("@default", "Time") },
0230     { "TKWD", QT_TRANSLATE_NOOP("@default", "Podcast Keywords") },
0231     { "TLEN", QT_TRANSLATE_NOOP("@default", "Length") },
0232     { "TOFN", QT_TRANSLATE_NOOP("@default", "Original Filename") },
0233     { "TOWN", QT_TRANSLATE_NOOP("@default", "File Owner") },
0234     { "TPRO", QT_TRANSLATE_NOOP("@default", "Produced Notice") },
0235     { "TRDA", QT_TRANSLATE_NOOP("@default", "Recording Date") },
0236     { "TRSN", QT_TRANSLATE_NOOP("@default", "Radio Station Name") },
0237     { "TRSO", QT_TRANSLATE_NOOP("@default", "Radio Station Owner") },
0238     { "TSIZ", QT_TRANSLATE_NOOP("@default", "Size") },
0239     { "TXXX", QT_TRANSLATE_NOOP("@default", "User-defined Text") },
0240     { "UFID", QT_TRANSLATE_NOOP("@default", "Unique File Identifier") },
0241     { "USER", QT_TRANSLATE_NOOP("@default", "Terms of Use") },
0242     { "WCOM", QT_TRANSLATE_NOOP("@default", "Commercial URL") },
0243     { "WCOP", QT_TRANSLATE_NOOP("@default", "Copyright URL") },
0244     { "WFED", QT_TRANSLATE_NOOP("@default", "Podcast Feed") },
0245     { "WORS", QT_TRANSLATE_NOOP("@default", "Official Radio Station") },
0246     { "WPAY", QT_TRANSLATE_NOOP("@default", "Payment") },
0247     { "WPUB", QT_TRANSLATE_NOOP("@default", "Official Publisher") },
0248     { "WXXX", QT_TRANSLATE_NOOP("@default", "User-defined URL") },
0249     { "BAND", QT_TRANSLATE_NOOP("@default", "Album Artist") },
0250     { "CONTACT", QT_TRANSLATE_NOOP("@default", "Contact") },
0251     { "CONTENTGROUP", QT_TRANSLATE_NOOP("@default", "Grouping") },
0252     { "DESCRIPTION", QT_TRANSLATE_NOOP("@default", "Description") },
0253     { "DISCTOTAL", QT_TRANSLATE_NOOP("@default", "Total Discs") },
0254     { "ENCODER", QT_TRANSLATE_NOOP("@default", "Encoder") },
0255     { "ENCODER_OPTIONS", QT_TRANSLATE_NOOP("@default", "Encoder Settings") },
0256     { "ENCODEDBY", QT_TRANSLATE_NOOP("@default", "Encoded-by") },
0257     { "ENCODING", QT_TRANSLATE_NOOP("@default", "Encoding") },
0258     { "ENGINEER", QT_TRANSLATE_NOOP("@default", "Engineer") },
0259     { "ENSEMBLE", QT_TRANSLATE_NOOP("@default", "Ensemble") },
0260     { "GUESTARTIST", QT_TRANSLATE_NOOP("@default", "Guest Artist") },
0261     { "IsVBR", QT_TRANSLATE_NOOP("@default", "VBR") },
0262     { "iTunPGAP", QT_TRANSLATE_NOOP("@default", "Gapless Playback") },
0263     { "LABEL", QT_TRANSLATE_NOOP("@default", "Label") },
0264     { "LABELNO", QT_TRANSLATE_NOOP("@default", "Label Number") },
0265     { "LICENSE", QT_TRANSLATE_NOOP("@default", "License") },
0266     { "LOCATION", QT_TRANSLATE_NOOP("@default", "Location") },
0267     { "OPUS", QT_TRANSLATE_NOOP("@default", "Opus") },
0268     { "ORIGARTIST", QT_TRANSLATE_NOOP("@default", "Original Artist") },
0269     { "ORGANIZATION", QT_TRANSLATE_NOOP("@default", "Organization") },
0270     { "PARTNUMBER", QT_TRANSLATE_NOOP("@default", "Part Number") },
0271     { "PRODUCER", QT_TRANSLATE_NOOP("@default", "Producer") },
0272     { "PRODUCTNUMBER", QT_TRANSLATE_NOOP("@default", "Product Number") },
0273     { "RECORDINGDATE", QT_TRANSLATE_NOOP("@default", "Recording Date") },
0274     { "REMIXEDBY", QT_TRANSLATE_NOOP("@default", "Remixer") },
0275     { "TOTALDISCS", QT_TRANSLATE_NOOP("@default", "Total Discs") },
0276     { "TOTALTRACKS", QT_TRANSLATE_NOOP("@default", "Total Tracks") },
0277     { "TRACKTOTAL", QT_TRANSLATE_NOOP("@default", "Total Tracks") },
0278     { "UNKNOWN", QT_TRANSLATE_NOOP("@default", "Unknown") },
0279     { "Unknown", QT_TRANSLATE_NOOP("@default", "Unknown") },
0280     { "VERSION", QT_TRANSLATE_NOOP("@default", "Version") },
0281     { "VOLUME", QT_TRANSLATE_NOOP("@default", "Volume") },
0282     { "WWW", QT_TRANSLATE_NOOP("@default", "User-defined URL") },
0283     { "WM/AlbumArtistSortOrder", QT_TRANSLATE_NOOP("@default", "Sort Album Artist") },
0284     { "WM/Comments", QT_TRANSLATE_NOOP("@default", "Comment") },
0285     { "WM/MCDI", QT_TRANSLATE_NOOP("@default", "MCDI") },
0286     { "WM/Mood", QT_TRANSLATE_NOOP("@default", "Mood") },
0287     { "WM/OriginalFilename", QT_TRANSLATE_NOOP("@default", "Original Filename") },
0288     { "WM/OriginalLyricist", QT_TRANSLATE_NOOP("@default", "Original Lyricist") },
0289     { "WM/PromotionURL", QT_TRANSLATE_NOOP("@default", "Commercial URL") },
0290     { "WM/SharedUserRating", QT_TRANSLATE_NOOP("@default", "User Rating") },
0291     { "WM/UserWebURL", QT_TRANSLATE_NOOP("@default", "User-defined URL") },
0292     { "akID", QT_TRANSLATE_NOOP("@default", "Account Type") },
0293     { "apID", QT_TRANSLATE_NOOP("@default", "Purchase Account") },
0294     { "atID", QT_TRANSLATE_NOOP("@default", "Artist ID") },
0295     { "catg", QT_TRANSLATE_NOOP("@default", "Category") },
0296     { "cnID", QT_TRANSLATE_NOOP("@default", "Catalog ID") },
0297     { "cond", QT_TRANSLATE_NOOP("@default", "Conductor") },
0298     { "desc", QT_TRANSLATE_NOOP("@default", "Description") },
0299     { "geID", QT_TRANSLATE_NOOP("@default", "Genre ID") },
0300     { "hdvd", QT_TRANSLATE_NOOP("@default", "HD Video") },
0301     { "keyw", QT_TRANSLATE_NOOP("@default", "Keyword") },
0302     { "ldes", QT_TRANSLATE_NOOP("@default", "Long Description") },
0303     { "pcst", QT_TRANSLATE_NOOP("@default", "Podcast") },
0304     { "pgap", QT_TRANSLATE_NOOP("@default", "Gapless Playback") },
0305     { "plID", QT_TRANSLATE_NOOP("@default", "Album ID") },
0306     { "purd", QT_TRANSLATE_NOOP("@default", "Purchase Date") },
0307     { "rtng", QT_TRANSLATE_NOOP("@default", "Rating/Advisory") },
0308     { "sfID", QT_TRANSLATE_NOOP("@default", "Country Code") },
0309     { "sosn", QT_TRANSLATE_NOOP("@default", "Sort Show") },
0310     { "stik", QT_TRANSLATE_NOOP("@default", "Media Type") },
0311     { "tven", QT_TRANSLATE_NOOP("@default", "TV Episode") },
0312     { "tves", QT_TRANSLATE_NOOP("@default", "TV Episode Number") },
0313     { "tvnn", QT_TRANSLATE_NOOP("@default", "TV Network Name") },
0314     { "tvsh", QT_TRANSLATE_NOOP("@default", "TV Show Name") },
0315     { "tvsn", QT_TRANSLATE_NOOP("@default", "TV Season") },
0316     { "year", QT_TRANSLATE_NOOP("@default", "Year") },
0317     { "\251mvn", QT_TRANSLATE_NOOP("@default", "Movement Name") },
0318     { "\251mvi", QT_TRANSLATE_NOOP("@default", "Movement Number") },
0319     { "\251mvc", QT_TRANSLATE_NOOP("@default", "Movement Count") },
0320     { "shwm", QT_TRANSLATE_NOOP("@default", "Show Work & Movement") },
0321     { "ownr", QT_TRANSLATE_NOOP("@default", "Owner") },
0322     { "purl", QT_TRANSLATE_NOOP("@default", "Podcast URL") },
0323     { "egid", QT_TRANSLATE_NOOP("@default", "Podcast GUID") },
0324     { "cmID", QT_TRANSLATE_NOOP("@default", "Composer ID") },
0325     { "xid ", QT_TRANSLATE_NOOP("@default", "XID") },
0326     { "Chapters", QT_TRANSLATE_NOOP("@default", "Chapters") },
0327     { "IARL", QT_TRANSLATE_NOOP("@default", "Archival Location") },
0328     { "ICMS", QT_TRANSLATE_NOOP("@default", "Commissioned") },
0329     { "ICRP", QT_TRANSLATE_NOOP("@default", "Cropped") },
0330     { "IDIM", QT_TRANSLATE_NOOP("@default", "Dimensions") },
0331     { "IDPI", QT_TRANSLATE_NOOP("@default", "Dots Per Inch") },
0332     { "IKEY", QT_TRANSLATE_NOOP("@default", "Keywords") },
0333     { "ILGT", QT_TRANSLATE_NOOP("@default", "Lightness") },
0334     { "IPLT", QT_TRANSLATE_NOOP("@default", "Number of Colors") },
0335     { "ISBJ", QT_TRANSLATE_NOOP("@default", "Subject") },
0336     { "ISHP", QT_TRANSLATE_NOOP("@default", "Sharpness") },
0337     { "ISRF", QT_TRANSLATE_NOOP("@default", "Source Form") }
0338   };
0339   static QMap<QByteArray, QByteArray> idStrMap;
0340   if (idStrMap.isEmpty()) {
0341     // first time initialization
0342     for (const auto& [id, str] : strOfId) {
0343       idStrMap.insert(id, str);
0344     }
0345   }
0346   return idStrMap;
0347 }
0348 
0349 /**
0350  * Get a reduced field list without fields which are only supported by a
0351  * specific tag format.
0352  * @param fieldList original field list
0353  * @return reduced field list.
0354  */
0355 Frame::FieldList reducedFieldList(const Frame::FieldList& fieldList)
0356 {
0357   Frame::FieldList reduced;
0358   for (const Frame::Field& fld : fieldList) {
0359     if (fld.m_id != Frame::ID_ImageFormat &&
0360         fld.m_id != Frame::ID_ImageProperties) {
0361       reduced.append(fld);
0362     }
0363   }
0364   return reduced;
0365 }
0366 
0367 }
0368 
0369 Frame::ExtendedType::ExtendedType(const QString& name) :
0370   m_type(getTypeFromName(name)), m_name(name)
0371 {
0372 }
0373 
0374 Frame::ExtendedType::ExtendedType(Type type) :
0375   m_type(type), m_name(QString::fromLatin1(getNameFromType(type)))
0376 {
0377 }
0378 
0379 /**
0380  * Get name of type.
0381  * @return name.
0382  */
0383 QString Frame::ExtendedType::getName() const
0384 {
0385   return m_type != FT_Other ? QString::fromLatin1(getNameFromType(m_type))
0386                             : m_name;
0387 }
0388 
0389 /**
0390  * Get translated name of type.
0391  * @return name.
0392  */
0393 QString Frame::ExtendedType::getTranslatedName() const
0394 {
0395   return m_type != FT_Other
0396       ? QCoreApplication::translate("@default", getNameFromType(m_type))
0397       : m_name;
0398 }
0399 
0400 
0401 /**
0402  * Constructor.
0403  */
0404 Frame::Frame()
0405   : m_index(-1), m_marked(FrameNotice::None), m_valueChanged(false)
0406 {
0407 }
0408 
0409 /**
0410  * Constructor.
0411  */
0412 Frame::Frame(Type type, const QString& value,
0413              const QString& name, int index)
0414   : m_extendedType(type, name), m_index(index), m_value(value),
0415     m_marked(FrameNotice::None), m_valueChanged(false)
0416 {
0417 }
0418 
0419 /**
0420  * Constructor.
0421  * @param type  type and internal name
0422  * @param value value
0423  * @param index index inside tag, -1 if unknown
0424  */
0425 Frame::Frame(const ExtendedType& type, const QString& value, int index)
0426   : m_extendedType(type), m_index(index), m_value(value),
0427     m_marked(FrameNotice::None), m_valueChanged(false)
0428 {
0429 }
0430 
0431 /**
0432  * Set the value from a field in the field list.
0433  */
0434 void Frame::setValueFromFieldList()
0435 {
0436   if (!getFieldList().empty()) {
0437     for (auto fldIt = getFieldList().constBegin();
0438          fldIt != getFieldList().constEnd();
0439          ++fldIt) {
0440       if (int id = fldIt->m_id;
0441           id == ID_Text ||
0442           id == ID_Description ||
0443           id == ID_Url) {
0444         m_value = fldIt->m_value.toString();
0445         if (id == ID_Text) {
0446           // highest priority, will not be overwritten
0447           break;
0448         }
0449       }
0450     }
0451   }
0452 }
0453 
0454 /**
0455  * Set a field in the field list from the value.
0456  */
0457 void Frame::setFieldListFromValue()
0458 {
0459   if (!fieldList().empty()) {
0460     auto it = fieldList().end(); // clazy:exclude=detaching-member
0461     for (auto fldIt = fieldList().begin(); fldIt != fieldList().end(); ++fldIt) { // clazy:exclude=detaching-member
0462       if (int id = fldIt->m_id;
0463           id == ID_Text ||
0464           id == ID_Description ||
0465           id == ID_Url) {
0466         it = fldIt;
0467         if (id == ID_Text) {
0468           // highest priority, will not be overwritten
0469           break;
0470         }
0471       } else if (id == ID_Rating) {
0472         bool ok;
0473         int rating = m_value.toInt(&ok);
0474         if (ok) {
0475           fldIt->m_value = rating;
0476           break;
0477         }
0478       }
0479     }
0480     if (it != fieldList().end()) {
0481       it->m_value = m_value;
0482     }
0483   }
0484 }
0485 
0486 /**
0487  * Split a string into a string list using stringListSeparator().
0488  * @param str string to split at stringListSeparator(), with support of
0489  *            '\' as an escape character.
0490  * @return list of strings split at separator character not prefixed with
0491  *         escape character.
0492  */
0493 QStringList Frame::splitStringList(const QString& str)
0494 {
0495   static QRegularExpression separatorRe(
0496       QLatin1String("(?<!\\\\)\\") + stringListSeparator());
0497   static const QChar sep = stringListSeparator();
0498   static const QString escSep = QLatin1String("\\") + stringListSeparator();
0499   QStringList strs = str.split(separatorRe);
0500   for (QString& s : strs) {
0501     s.replace(escSep, sep);
0502   }
0503   return strs;
0504 }
0505 
0506 /**
0507  * Join a string list using stringListSeparator().
0508  * @param strs strings to join, if they contain the separator character,
0509  *             it will be escaped with '\'.
0510  * @return escaped strings joined by separator characters.
0511  */
0512 QString Frame::joinStringList(const QStringList& strs)
0513 {
0514   static const QChar sep = stringListSeparator();
0515   static const QString escSep = QLatin1String("\\") + stringListSeparator();
0516   QStringList escapedStrs(strs);
0517   for (QString& str : escapedStrs) {
0518     str.replace(sep, escSep);
0519   }
0520   return escapedStrs.join(sep);
0521 }
0522 
0523 /**
0524  * Convert string (e.g. "track/total number of tracks") to number.
0525  *
0526  * @param str string to convert
0527  * @param ok  if not 0, true is returned here if conversion is ok
0528  *
0529  * @return number in string ignoring total after slash.
0530  */
0531 int Frame::numberWithoutTotal(const QString& str, bool* ok)
0532 {
0533   int slashPos = str.indexOf(QLatin1Char('/'));
0534 #if QT_VERSION >= 0x060000
0535   return slashPos == -1 ? str.toInt(ok) : str.left(slashPos).toInt(ok);
0536 #else
0537   return slashPos == -1 ? str.toInt(ok) : str.leftRef(slashPos).toInt(ok);
0538 #endif
0539 }
0540 
0541 /**
0542  * Get value as integer.
0543  * @return value.
0544  */
0545 int Frame::getValueAsNumber() const
0546 {
0547   if (isInactive()) {
0548     return -1;
0549   }
0550   if (isEmpty()) {
0551     return 0;
0552   }
0553   return numberWithoutTotal(m_value);
0554 }
0555 
0556 /**
0557  * Set value as integer.
0558  * @param n value as number
0559  */
0560 void Frame::setValueAsNumber(int n)
0561 {
0562   if (n == -1) {
0563     m_value = QString();
0564   } else if (n == 0) {
0565     m_value = QLatin1String("");
0566   } else {
0567     m_value.setNum(n);
0568   }
0569 }
0570 
0571 /**
0572  * Check if the value of this frame is fuzzy equal to another frame.
0573  * Other than with strict equality, total values and some fields which are
0574  * not supported in all tag formats are ignored.
0575  * @param other frame to compare
0576  * @return true if more or less equal.
0577  */
0578 bool Frame::isFuzzyEqual(const Frame& other) const
0579 {
0580   if (getType() == FT_Track || getType() == FT_Disc) {
0581     return getValueAsNumber() == other.getValueAsNumber();
0582   }
0583   return getValue() == other.getValue() &&
0584          (getFieldList().isEmpty() ||
0585           other.getFieldList().isEmpty() ||
0586           Field::fuzzyCompareFieldLists(getFieldList(),
0587                                         other.getFieldList()));
0588 }
0589 
0590 /**
0591  * Get the value of a field.
0592  *
0593  * @param id field ID
0594  *
0595  * @return field value, invalid if field not found.
0596  */
0597 QVariant Frame::getFieldValue(FieldId id) const
0598 {
0599   for (auto it = m_fieldList.constBegin(); it != m_fieldList.constEnd(); ++it) {
0600     if (it->m_id == id) {
0601       return it->m_value;
0602     }
0603   }
0604   return QVariant();
0605 }
0606 
0607 /**
0608  * Set value as string and mark it as changed if it is changed.
0609  * This method will avoid setting "different" representations.
0610  * @param value value as string
0611  */
0612 void Frame::setValueIfChanged(const QString& value)
0613 {
0614   if (value != differentRepresentation()) {
0615     if (QString oldValue(getValue());
0616         value != oldValue && !(value.isEmpty() && oldValue.isEmpty())) {
0617       setValue(value);
0618       setValueChanged();
0619     }
0620   }
0621 }
0622 
0623 /**
0624  * Read value text from file.
0625  * @param fileName name of data file
0626  * @return true if file read and value set.
0627  */
0628 bool Frame::setValueFromFile(const QString& fileName)
0629 {
0630   if (!fileName.isEmpty()) {
0631     if (QFile file(fileName); file.open(QIODevice::ReadOnly)) {
0632       QString value;
0633       QByteArray data = file.readAll();
0634 #if QT_VERSION >= 0x060000
0635       auto toUtf8 = QStringDecoder(QStringConverter::Utf8);
0636       value = toUtf8(data);
0637       if (toUtf8.hasError()) {
0638         auto encoding = QStringConverter::encodingForData(data);
0639         auto decoder = QStringDecoder(encoding.value_or(QStringConverter::Latin1));
0640         value = decoder(data);
0641       }
0642 #else
0643       QTextCodec::ConverterState state;
0644       if (QTextCodec* codec = QTextCodec::codecForName("UTF-8")) {
0645         value = codec->toUnicode(data.constData(), data.size(), &state);
0646         if (state.invalidChars > 0) {
0647           codec = QTextCodec::codecForUtfText(
0648                 data, QTextCodec::codecForName("ISO 8859-1"));
0649           if (codec) {
0650             value = codec->toUnicode(data.constData(), data.size());
0651           }
0652         }
0653       }
0654 #endif
0655       setValueIfChanged(value);
0656       file.close();
0657       return true;
0658     }
0659   }
0660   return false;
0661 }
0662 
0663 /**
0664  * Save value text to a file.
0665  * @param fileName name of data file to save
0666  * @return true if saved.
0667  */
0668 bool Frame::writeValueToFile(const QString& fileName) const
0669 {
0670   if (!fileName.isEmpty()) {
0671     if (QFile file(fileName); file.open(QIODevice::WriteOnly)) {
0672       file.write(m_value.toUtf8());
0673       file.close();
0674       return true;
0675     }
0676   }
0677   return false;
0678 }
0679 
0680 /**
0681  * Check if the fields in another frame are equal.
0682  *
0683  * @param other other frame
0684  *
0685  * @return true if equal.
0686  */
0687 bool Frame::isEqual(const Frame& other) const
0688 {
0689   if (getType() != other.getType() || getValue() != other.getValue())
0690     return false;
0691 
0692   const FieldList& otherFieldList = other.getFieldList();
0693   if (m_fieldList.size() != otherFieldList.size())
0694     return false;
0695 
0696   for (auto thisIt = m_fieldList.constBegin(), otherIt = otherFieldList.constBegin();
0697        thisIt != m_fieldList.constEnd() && otherIt != otherFieldList.constEnd();
0698        ++thisIt, ++otherIt) {
0699     if (thisIt->m_id != otherIt->m_id || thisIt->m_value != otherIt->m_value) {
0700       return false;
0701     }
0702   }
0703 
0704   return true;
0705 }
0706 
0707 /**
0708  * Set value of a field.
0709  *
0710  * @param frame frame to set
0711  * @param id    field ID
0712  * @param value field value
0713  *
0714  * @return true if field found and set.
0715  */
0716 bool Frame::setField(Frame& frame, FieldId id, const QVariant& value)
0717 {
0718   for (auto it = frame.fieldList().begin(); it != frame.fieldList().end(); ++it) { // clazy:exclude=detaching-member
0719     if (it->m_id == id) {
0720       it->m_value = value;
0721       if (id == ID_Description) frame.setValue(value.toString());
0722       return true;
0723     }
0724   }
0725   return false;
0726 }
0727 
0728 /**
0729  * Set value of a field.
0730  *
0731  * @param frame frame to set
0732  * @param fieldName name of field, can be English or translated
0733  * @param value field value
0734  *
0735  * @return true if field found and set.
0736  */
0737 bool Frame::setField(Frame& frame, const QString& fieldName,
0738                      const QVariant& value)
0739 {
0740   if (const FieldId id = Field::getFieldId(fieldName); id != ID_NoField) {
0741 #if QT_VERSION >= 0x060000
0742     QMetaType valueType = value.metaType();
0743     QMetaType fieldType;
0744 #else
0745     QVariant::Type valueType = value.type();
0746     QVariant::Type fieldType;
0747 #endif
0748     switch (id) {
0749     case ID_TextEnc:
0750     case ID_PictureType:
0751     case ID_Counter:
0752     case ID_VolumeAdj:
0753     case ID_NumBits:
0754     case ID_VolChgRight:
0755     case ID_VolChgLeft:
0756     case ID_PeakVolRight:
0757     case ID_PeakVolLeft:
0758     case ID_TimestampFormat:
0759     case ID_ContentType:
0760 #if QT_VERSION >= 0x060000
0761       fieldType = QMetaType(QMetaType::Int);
0762 #else
0763       fieldType = QVariant::Int;
0764 #endif
0765       break;
0766     case ID_Data:
0767 #if QT_VERSION >= 0x060000
0768       fieldType = QMetaType(QMetaType::QByteArray);
0769 #else
0770       fieldType = QVariant::ByteArray;
0771 #endif
0772       break;
0773     default:
0774 #if QT_VERSION >= 0x060000
0775       fieldType = QMetaType(QMetaType::QString);
0776 #else
0777       fieldType = QVariant::String;
0778 #endif
0779     }
0780     if (valueType != fieldType && value.canConvert(fieldType)) {
0781       if (QVariant converted(value); converted.convert(fieldType)) {
0782         return setField(frame, id, converted);
0783       }
0784     }
0785     return setField(frame, id, value);
0786   }
0787   return false;
0788 }
0789 
0790 /**
0791  * Get value of a field.
0792  *
0793  * @param frame frame to get
0794  * @param id    field ID
0795  *
0796  * @return field value, invalid if not found.
0797  */
0798 QVariant Frame::getField(const Frame& frame, FieldId id)
0799 {
0800   QVariant result;
0801   if (!frame.getFieldList().empty()) {
0802     for (auto it = frame.getFieldList().constBegin();
0803          it != frame.getFieldList().constEnd();
0804          ++it) {
0805       if (it->m_id == id) {
0806         result = it->m_value;
0807         break;
0808       }
0809     }
0810   }
0811   return result;
0812 }
0813 
0814 /**
0815  * Get value of a field.
0816  *
0817  * @param frame frame to get
0818  * @param fieldName name of field, can be English or translated
0819  *
0820  * @return field value, invalid if not found.
0821  */
0822 QVariant Frame::getField(const Frame& frame, const QString& fieldName)
0823 {
0824   const FieldId id = Field::getFieldId(fieldName);
0825   return id != ID_NoField ? getField(frame, id) : QVariant();
0826 }
0827 
0828 /**
0829  * Get type of frame from English name.
0830  *
0831  * @param name name, spaces and case are ignored
0832  *
0833  * @return type.
0834  */
0835 Frame::Type Frame::getTypeFromName(const QString& name)
0836 {
0837   static QMap<QString, int> strNumMap;
0838   if (strNumMap.empty()) {
0839     // first time initialization
0840     for (int i = 0; i < Frame::FT_Custom1; ++i) {
0841       auto type = static_cast<Frame::Type>(i);
0842       strNumMap.insert(QString::fromLatin1(getNameFromType(type))
0843                        .remove(QLatin1Char(' ')).toUpper(), type);
0844     }
0845   }
0846   QString ucName(name.toUpper());
0847   ucName.remove(QLatin1Char(' '));
0848   if (auto it = strNumMap.constFind(ucName); it != strNumMap.constEnd()) {
0849     return static_cast<Frame::Type>(*it);
0850   }
0851   return getTypeFromCustomFrameName(name.toLatin1());
0852 }
0853 
0854 /**
0855  * Get a translated string for a frame type.
0856  *
0857  * @param type frame type
0858  *
0859  * @return frame type, null string if unknown.
0860  */
0861 QString Frame::getFrameTypeName(Type type)
0862 {
0863   return QCoreApplication::translate("@default", getNameFromType(type));
0864 }
0865 
0866 /**
0867  * Get a display name for a frame name.
0868  * @param name frame name as returned by getName()
0869  * @return display name, transformed if necessary and translated.
0870  */
0871 QString Frame::getDisplayName(const QString& name)
0872 {
0873   QMap<QByteArray, QByteArray> idStrMap = getDisplayNamesOfIds();
0874   if (name.isEmpty())
0875     return name;
0876 
0877   if (Type type = getTypeFromName(name); !Frame::isCustomFrameTypeOrOther(type))
0878     return QCoreApplication::translate("@default",
0879                                        name.toLatin1().constData());
0880 
0881   QString nameStr(name);
0882   if (int nlPos = nameStr.indexOf(QLatin1Char('\n')); nlPos > 0)
0883     // probably "TXXX - User defined text information\nDescription" or
0884     // "WXXX - User defined URL link\nDescription"
0885     nameStr = nameStr.mid(nlPos + 1);
0886 
0887   QByteArray id;
0888   if (nameStr.mid(4, 3) == QLatin1String(" - ")) {
0889     id = nameStr.left(4).toLatin1();
0890   } else {
0891     id = nameStr.toLatin1();
0892   }
0893 
0894   if (auto it = idStrMap.constFind(id); it != idStrMap.constEnd()) {
0895     return QCoreApplication::translate("@default", it->constData());
0896   }
0897   return nameStr;
0898 }
0899 
0900 /**
0901  * Get a map with display names as keys and frame names as values.
0902  * @param names frame names as returned by getName()
0903  * @return mapping of display names to frame names.
0904  */
0905 QMap<QString, QString> Frame::getDisplayNameMap(const QStringList& names)
0906 {
0907   QMap<QString, QString> map;
0908   for (const QString& name : names) {
0909     map.insert(getDisplayName(name), name);
0910   }
0911   return map;
0912 }
0913 
0914 /**
0915  * Get the frame name for a translated display name.
0916  * @param name translated display name
0917  * @return English frame name for @a name if found, else @a name.
0918  */
0919 QString Frame::getNameForTranslatedFrameName(const QString& name)
0920 {
0921   static QMap<QString, QString> nameMap;
0922   if (nameMap.isEmpty()) {
0923     // first time initialization
0924     for (int k = Frame::FT_FirstFrame; k < Frame::FT_Custom1; ++k) {
0925       QString typeName = Frame::ExtendedType(static_cast<Frame::Type>(k),
0926                                          QLatin1String("")).getName();
0927       nameMap.insert(QCoreApplication::translate("@default",
0928                          typeName.toLatin1().constData()), typeName);
0929     }
0930     QMap<QByteArray, QByteArray> idStrMap = getDisplayNamesOfIds();
0931     const auto names = idStrMap.values();
0932     for (const QByteArray& frameName : names) {
0933       nameMap.insert(QCoreApplication::translate("@default", frameName),
0934                      QString::fromLatin1(frameName));
0935     }
0936   }
0937   return nameMap.value(name, name);
0938 }
0939 
0940 /**
0941  * Get internal frame ID of non unified frame for a translated display name.
0942  * @param name translated display name
0943  * @return internal frame ID, e.g. "SYLT", "keyw" of non unified frame,
0944  *         null if @a name is not the name of a supported non unified frame.
0945  */
0946 QByteArray Frame::getFrameIdForTranslatedFrameName(const QString& name)
0947 {
0948   static QMap<QString, QByteArray> nameMap;
0949   if (nameMap.isEmpty()) {
0950     // first time initialization
0951     const QMap<QByteArray, QByteArray> idStrMap = getDisplayNamesOfIds();
0952     for (auto it = idStrMap.constBegin(); it != idStrMap.constEnd(); ++it) {
0953       nameMap.insert(QCoreApplication::translate("@default", it.value()),
0954                      it.key());
0955     }
0956   }
0957   return nameMap.value(name);
0958 }
0959 
0960 /**
0961  * Get the internal name for a custom frame.
0962  * @param type custom frame type (FT_Custom1..FT_LastFrame)
0963  * @return custom frame name, empty if not used.
0964  */
0965 QByteArray Frame::getNameForCustomFrame(Frame::Type type)
0966 {
0967   if (int idx = type - FT_Custom1; idx >= 0 && idx < customFrameNames.size()) {
0968     return customFrameNames.at(idx);
0969   }
0970   return "";
0971 }
0972 
0973 /**
0974  * Get type of frame from custom frame name.
0975  * @param name custom frame name
0976  * @return type, FT_Other if no custom frame with @a name exists.
0977  */
0978 Frame::Type Frame::getTypeFromCustomFrameName(const QByteArray& name)
0979 {
0980   if (customFrameNameMap.isEmpty()) {
0981     // first time initialization
0982     for (int i = 0; i < customFrameNames.size(); ++i) {
0983       auto type = static_cast<Frame::Type>(FT_Custom1 + i);
0984       if (QByteArray customFrameName = customFrameNames.at(i).toUpper()
0985             .replace(' ', QByteArray());
0986           !customFrameName.isEmpty()) {
0987         customFrameNameMap.insert(customFrameName, type);
0988       }
0989     }
0990   }
0991   auto ucName = name.toUpper().replace(' ', QByteArray());
0992   if (auto it = customFrameNameMap.constFind(ucName);
0993       it != customFrameNameMap.constEnd()) {
0994     return static_cast<Frame::Type>(*it);
0995   }
0996   return Frame::FT_Other;
0997 }
0998 
0999 /**
1000  * Set the internal names for all custom frames.
1001  * The number of custom frames is limited. The internal vector will be resized
1002  * to fit the fixed number of custom frames.
1003  *
1004  * @param customNames names for the custom frame types (FT_Custom1...),
1005  * leading '!' will be removed from the names
1006  * @return true if custom frame names were changed.
1007  */
1008 bool Frame::setNamesForCustomFrames(const QStringList& customNames)
1009 {
1010   QVector<QByteArray> newCustomFrameNames(NUM_CUSTOM_FRAME_NAMES);
1011   int i = 0;
1012   for (auto it = customNames.constBegin(); it != customNames.constEnd(); ++it) {
1013     if (i >= NUM_CUSTOM_FRAME_NAMES) {
1014       break;
1015     }
1016     QString name = *it;
1017     if (name.startsWith(QLatin1Char('!'))) {
1018       name.remove(0, 1);
1019     }
1020     if (!name.isEmpty()) {
1021       newCustomFrameNames[i++] = name.toLatin1();
1022     }
1023   }
1024   if (customFrameNames != newCustomFrameNames) {
1025     customFrameNames.swap(newCustomFrameNames);
1026     // Invalidate mapping used by getTypeFromName()
1027     customFrameNameMap.clear();
1028     return true;
1029   }
1030   return false;
1031 }
1032 
1033 /**
1034  * Get the names for all custom frames.
1035  * The returned list does not contain empty names.
1036  *
1037  * @return names for the custom frame types (FT_Custom1, ...).
1038  */
1039 QStringList Frame::getNamesForCustomFrames()
1040 {
1041   QStringList names;
1042   for (auto it = customFrameNames.constBegin();
1043        it != customFrameNames.constEnd();
1044        ++it) {
1045     if (!it->isEmpty()) {
1046       names.append(QString::fromLatin1(*it));
1047     }
1048   }
1049   return names;
1050 }
1051 
1052 /**
1053  * Get a translated string for a field ID.
1054  *
1055  * @param type field ID type
1056  *
1057  * @return field ID type, null string if unknown.
1058  */
1059 QString Frame::Field::getFieldIdName(FieldId type)
1060 {
1061   Q_STATIC_ASSERT(std::size(fieldIdNames) == ID_Seller + 2);
1062   if (static_cast<int>(type) >= 0 &&
1063       static_cast<int>(type) < static_cast<int>(std::size(fieldIdNames) - 1)) {
1064     return QCoreApplication::translate("@default", fieldIdNames[type]);
1065   }
1066   return QString();
1067 }
1068 
1069 /**
1070  * List of field ID strings, NULL terminated.
1071  */
1072 const char* const* Frame::Field::getFieldIdNames()
1073 {
1074   return fieldIdNames;
1075 }
1076 
1077 /**
1078  * Get field ID from field name.
1079  * @param fieldName name of field, can be English or translated
1080  * @return field ID, ID_NoField if not found.
1081  */
1082 Frame::FieldId Frame::Field::getFieldId(const QString& fieldName)
1083 {
1084   const char* const* fn;
1085   int id;
1086   // First try to find an exact English match.
1087   for (fn = fieldIdNames, id = 0; *fn != nullptr; ++fn, ++id) {
1088     if (fieldName == QLatin1String(*fn)) {
1089       return static_cast<FieldId>(id);
1090     }
1091   }
1092   // Then try to find a lowercase match ignoring spaces.
1093   QString lcName = fieldName.toLower().remove(QLatin1Char(' '));
1094   for (fn = fieldIdNames, id = 0; *fn != nullptr; ++fn, ++id) {
1095     if (lcName ==  QString::fromLatin1(*fn).toLower().remove(QLatin1Char(' '))) {
1096       return static_cast<FieldId>(id);
1097     }
1098   }
1099   // Finally try to find a translated name.
1100   for (fn = fieldIdNames, id = 0; *fn != nullptr; ++fn, ++id) {
1101     if (fieldName == QCoreApplication::translate("@default", *fn)) {
1102       return static_cast<FieldId>(id);
1103     }
1104   }
1105   return ID_NoField;
1106 }
1107 
1108 /**
1109  * Get a translated string for a text encoding.
1110  *
1111  * @param type text encoding type
1112  *
1113  * @return text encoding type, null string if unknown.
1114  */
1115 QString Frame::Field::getTextEncodingName(TextEncoding type)
1116 {
1117   if (static_cast<int>(type) >= 0 &&
1118       static_cast<int>(type) < static_cast<int>(
1119         std::size(textEncodingNames) - 1)) {
1120     return QCoreApplication::translate("@default", textEncodingNames[type]);
1121   }
1122   return QString();
1123 }
1124 
1125 /**
1126  * List of text encoding strings, NULL terminated.
1127  */
1128 const char* const* Frame::Field::getTextEncodingNames()
1129 {
1130   return textEncodingNames;
1131 }
1132 
1133 /**
1134  * Get a translated string for a timestamp format.
1135  *
1136  * @param type timestamp format type
1137  *
1138  * @return timestamp format type, null string if unknown.
1139  */
1140 QString Frame::Field::getTimestampFormatName(int type)
1141 {
1142   if (type >= 0 &&
1143       static_cast<unsigned int>(type) < std::size(timestampFormatNames) - 1) {
1144     return QCoreApplication::translate("@default", timestampFormatNames[type]);
1145   }
1146   return QString();
1147 }
1148 
1149 /**
1150  * List of timestamp format strings, NULL terminated.
1151  */
1152 const char* const* Frame::Field::getTimestampFormatNames()
1153 {
1154   return timestampFormatNames;
1155 }
1156 
1157 /**
1158  * Get a translated string for a content type.
1159  *
1160  * @param type content type
1161  *
1162  * @return content type, null string if unknown.
1163  */
1164 QString Frame::Field::getContentTypeName(int type)
1165 {
1166   if (type >= 0 &&
1167       static_cast<unsigned int>(type) < std::size(contentTypeNames) - 1) {
1168     return QCoreApplication::translate("@default", contentTypeNames[type]);
1169   }
1170   return QString();
1171 }
1172 
1173 /**
1174  * List of content type strings, NULL terminated.
1175  */
1176 const char* const* Frame::Field::getContentTypeNames()
1177 {
1178   return contentTypeNames;
1179 }
1180 
1181 /**
1182  * Compare two field lists in a tolerant way.
1183  * This function can be used instead of the standard QList equality
1184  * operator if the field lists can be from different tag formats, which
1185  * may not all support the same field types.
1186  * @param fl1 first field list
1187  * @param fl2 second field list
1188  * @return true if they are similar enough.
1189  */
1190 bool Frame::Field::fuzzyCompareFieldLists(const QList<Field>& fl1,
1191                                           const QList<Field>& fl2)
1192 {
1193   return reducedFieldList(fl1) == reducedFieldList(fl2);
1194 }
1195 
1196 /**
1197  * Get list of available tag versions with translated description.
1198  * @return tag version/description pairs.
1199  */
1200 const QList<QPair<Frame::TagVersion, QString> > Frame::availableTagVersions()
1201 {
1202   QList<QPair<TagVersion, QString> > result;
1203   FOR_ALL_TAGS(tagNr) {
1204     const char* const tagStr = QT_TRANSLATE_NOOP("@default", "Tag %1");
1205     result << qMakePair(Frame::tagVersionCast(1 << tagNr),
1206                         QCoreApplication::translate("@default", tagStr)
1207                         .arg(Frame::tagNumberToString(tagNr)));
1208   }
1209   const char* const tag12Str = QT_TRANSLATE_NOOP("@default", "Tag 1 and Tag 2");
1210   result << qMakePair(TagV2V1, QCoreApplication::translate("@default",
1211                                                            tag12Str));
1212   if constexpr (TagVAll != TagV2V1) {
1213     const char* const allTagsStr = QT_TRANSLATE_NOOP("@default", "All Tags");
1214     result << qMakePair(TagVAll, QCoreApplication::translate("@default",
1215                                                              allTagsStr));
1216   }
1217   return result;
1218 }
1219 
1220 /**
1221  * Get string representation for tag number.
1222  * @param tagNr tag number
1223  * @return "1" for Tag_1, "2" for Tag_2, ..., null if invalid.
1224  */
1225 QString Frame::tagNumberToString(TagNumber tagNr)
1226 {
1227   return tagNr < Tag_NumValues ? QString::number(tagNr + 1) : QString();
1228 }
1229 
1230 /**
1231  * Get tag number from string representation.
1232  * @param str string representation
1233  * @return Tag_1 for "1", Tag_2 for "2", ..., Tag_NumValues if invalid.
1234  */
1235 Frame::TagNumber Frame::tagNumberFromString(const QString& str)
1236 {
1237   bool ok;
1238   int nr = str.toInt(&ok);
1239   return ok && --nr >= Frame::Tag_1 && nr < Frame::Tag_NumValues
1240       ? static_cast<Frame::TagNumber>(nr) : Tag_NumValues;
1241 }
1242 
1243 #ifndef QT_NO_DEBUG
1244 
1245 namespace {
1246 
1247 /**
1248  * Get string representation of variant.
1249  * @param val variant value
1250  * @return string representation.
1251  */
1252 QString variantToString(const QVariant& val)
1253 {
1254 #if QT_VERSION >= 0x060000
1255   if (val.typeId() == QMetaType::QByteArray) {
1256 #else
1257   if (val.type() == QVariant::ByteArray) {
1258 #endif
1259     return QString(QLatin1String("ByteArray of %1 bytes"))
1260         .arg(val.toByteArray().size());
1261   }
1262   return val.toString();
1263 }
1264 
1265 }
1266 
1267 /**
1268  * Dump contents of frame to debug console.
1269  */
1270 void Frame::dump() const
1271 {
1272   qDebug("Frame: name=%s, value=%s, type=%s, index=%d, valueChanged=%u, marked=%s",
1273          getInternalName().toLatin1().data(), m_value.toLatin1().data(),
1274          getNameFromType(getType()), m_index,
1275          m_valueChanged, qPrintable(m_marked.getDescription()));
1276   qDebug("  fields=");
1277   for (auto it = m_fieldList.constBegin(); it != m_fieldList.constEnd(); ++it) {
1278     qDebug("  Field: id=%s, value=%s",
1279            qPrintable(
1280              Field::getFieldIdName(static_cast<FieldId>(it->m_id))),
1281            variantToString(it->m_value).toLatin1().data());
1282   }
1283 }
1284 #endif
1285 
1286 
1287 /**
1288  * Bit mask containing the bits of all frame types which shall be used as
1289  * quick access frames.
1290  * This mask has to be handled like FrameFilter::m_enabledFrames.
1291  */
1292 quint64 FrameCollection::s_quickAccessFrames =
1293     FrameCollection::DEFAULT_QUICK_ACCESS_FRAMES;
1294 
1295 /**
1296  * Set values which are different inactive.
1297  *
1298  * @param others frames to compare, will be modified
1299  * @param differentValues optional storage for the different values
1300  */
1301 void FrameCollection::filterDifferent(
1302     FrameCollection& others,
1303     QHash<Frame::ExtendedType, QSet<QString>>* differentValues)
1304 {
1305   constexpr int ALREADY_HANDLED_INDEX = INT_MIN;
1306   QByteArray frameData, othersData;
1307   auto it = begin();
1308   while (it != end()) {
1309     auto& frame = const_cast<Frame&>(*it);
1310     // This frame list is not tied to a specific file, so the
1311     // index is not valid.
1312     frame.setIndex(-1);
1313     if (auto othersIt = others.find(frame); othersIt == others.end()) {
1314       frame.setDifferent();
1315       ++it;
1316     } else {
1317       while (it != end() && othersIt != others.end() &&
1318              !(frame < *it) && !(frame < *othersIt)) {
1319         if ((it->getType() != Frame::FT_Picture &&
1320              it->getValue() != othersIt->getValue()) ||
1321             (it->getType() == Frame::FT_Picture &&
1322              !(PictureFrame::getData(*it, frameData) &&
1323                PictureFrame::getData(*othersIt, othersData) &&
1324                frameData == othersData))) {
1325           if (differentValues && it->getType() != Frame::FT_Picture &&
1326               it->getType() != Frame::FT_Genre) {
1327             auto& valueSet = (*differentValues)[it->getExtendedType()];
1328             if (it->getValue() != Frame::differentRepresentation()) {
1329               valueSet.insert(it->getValue());
1330             }
1331             valueSet.insert(othersIt->getValue());
1332           }
1333           const_cast<Frame&>(*it).setDifferent();
1334         }
1335         // Mark as already handled.
1336         const_cast<Frame&>(*othersIt).setIndex(ALREADY_HANDLED_INDEX);
1337         ++it;
1338         ++othersIt;
1339       }
1340     }
1341   }
1342 
1343   // Insert frames which are in others but not in this (not marked as already
1344   // handled by index ALREADY_HANDLED_INDEX) as different frames.
1345   for (auto othersIt = others.begin();
1346        othersIt != others.end();
1347        ++othersIt) {
1348     if (othersIt->getIndex() != ALREADY_HANDLED_INDEX) {
1349       auto& frame = const_cast<Frame&>(*othersIt);
1350       frame.setIndex(-1);
1351       frame.setDifferent();
1352       insert(frame);
1353     }
1354   }
1355 }
1356 
1357 /**
1358  * Add standard frames which are missing.
1359  */
1360 void FrameCollection::addMissingStandardFrames()
1361 {
1362   quint64 mask;
1363   int i;
1364   for (i = Frame::FT_FirstFrame, mask = 1ULL;
1365        i <= Frame::FT_LastFrame;
1366        ++i, mask <<= 1) {
1367     if (s_quickAccessFrames & mask) {
1368       Frame frame(static_cast<Frame::Type>(i), QString(), QString(), -1);
1369       if (auto it = find(frame); it == end()) {
1370         insert(frame);
1371       }
1372     }
1373   }
1374 }
1375 
1376 /**
1377  * Copy enabled frames.
1378  *
1379  * @param flt filter with enabled frames
1380  *
1381  * @return copy with enabled frames.
1382  */
1383 FrameCollection FrameCollection::copyEnabledFrames(const FrameFilter& flt) const
1384 {
1385   FrameCollection frames;
1386   for (auto it = cbegin(); it != cend(); ++it) {
1387     if (flt.isEnabled(it->getType(), it->getName())) {
1388       Frame frame = *it;
1389       frame.setIndex(-1);
1390       frames.insert(frame);
1391     }
1392   }
1393   return frames;
1394 }
1395 
1396 /**
1397  * Remove all frames which are not enabled from the collection.
1398  *
1399  * @param flt filter with enabled frames
1400  */
1401 void FrameCollection::removeDisabledFrames(const FrameFilter& flt)
1402 {
1403   for (auto it = begin(); it != end();) {
1404     if (!flt.isEnabled(it->getType(), it->getName())) {
1405       erase(it++);
1406     } else {
1407       ++it;
1408     }
1409   }
1410 }
1411 
1412 /**
1413  * Set the index of all frames to -1.
1414  */
1415 void FrameCollection::setIndexesInvalid()
1416 {
1417   for (auto it = begin(); it != end(); ++it) {
1418     auto& frame = const_cast<Frame&>(*it);
1419     frame.setIndex(-1);
1420   }
1421 }
1422 
1423 /**
1424  * Copy frames which are empty or inactive from other frames.
1425  * This can be used to merge two frame collections.
1426  *
1427  * @param frames other frames
1428  */
1429 void FrameCollection::merge(const FrameCollection& frames)
1430 {
1431   for (auto otherIt = frames.cbegin(); otherIt != frames.cend(); ++otherIt) {
1432     if (auto it = find(*otherIt); it != end()) {
1433       QString value(otherIt->getValue());
1434       if (auto& frameFound = const_cast<Frame&>(*it);
1435           frameFound.getValue().isEmpty() && !value.isEmpty()) {
1436         frameFound.setValueIfChanged(value);
1437       }
1438     } else {
1439       Frame frame(*otherIt);
1440       frame.setIndex(-1);
1441       frame.setValueChanged(true);
1442       insert(frame);
1443     }
1444   }
1445 }
1446 
1447 /**
1448  * Check if the standard tags are empty or inactive.
1449  *
1450  * @return true if empty or inactive.
1451  */
1452 bool FrameCollection::isEmptyOrInactive() const
1453 {
1454   return
1455     getTitle().isEmpty() &&
1456     getArtist().isEmpty() &&
1457     getAlbum().isEmpty() &&
1458     getComment().isEmpty() &&
1459     getYear() <= 0 &&
1460     getTrack() <= 0 &&
1461     getGenre().isEmpty();
1462 }
1463 
1464 /**
1465  * Search for a frame only by name.
1466  *
1467  * @param name the name of the frame to find, a case-insensitive search for
1468  *             the first name starting with this string is performed
1469  *
1470  * @return iterator or end() if not found.
1471  */
1472 FrameCollection::const_iterator FrameCollection::searchByName(
1473     const QString& name) const
1474 {
1475   if (name.isEmpty())
1476     return cend();
1477 
1478   const_iterator it;
1479   QString ucName = name.toUpper().remove(QLatin1Char('/'));
1480   int len = ucName.length();
1481   for (it = cbegin(); it != cend(); ++it) {
1482     const QStringList names{it->getName(), it->getInternalName()};
1483     for (const QString& frameName : names) {
1484       QString ucFrameName(frameName.toUpper().remove(QLatin1Char('/')));
1485 #if QT_VERSION >= 0x060000
1486       if (ucName == ucFrameName.left(len))
1487 #else
1488       // Do not return ASF "Rating Information" when searching for "Rating".
1489       if (ucName == ucFrameName.leftRef(len) &&
1490           !(ucName == QLatin1String("RATING") &&
1491             ucFrameName == QLatin1String("RATING INFORMATION")))
1492 #endif
1493       {
1494         return it;
1495       }
1496       int nlPos = ucFrameName.indexOf(QLatin1Char('\n'));
1497 #if QT_VERSION >= 0x060000
1498       if (nlPos > 0 && ucName == ucFrameName.mid(nlPos + 1, len))
1499 #else
1500       if (nlPos > 0 && ucName == ucFrameName.midRef(nlPos + 1, len))
1501 #endif
1502       {
1503         // Description in TXXX, WXXX, COMM, PRIV matches
1504         return it;
1505       }
1506     }
1507   }
1508   return it;
1509 }
1510 
1511 /**
1512  * Find a frame by name.
1513  *
1514  * @param name  the name of the frame to find, if the exact name is not
1515  *              found, a case-insensitive search for the first name
1516  *              starting with this string is performed
1517  * @param index 0 for first frame with @a name, 1 for second, etc.
1518  *
1519  * @return iterator or end() if not found.
1520  */
1521 FrameCollection::const_iterator FrameCollection::findByName(
1522     const QString& name, int index) const
1523 {
1524   Frame frame(Frame::ExtendedType(name), QLatin1String(""), -1);
1525   auto it = find(frame);
1526   if (it == cend()) {
1527     it = searchByName(name);
1528     if (it == cend()) {
1529       const auto ids = getDisplayNamesOfIds().keys(name.toLatin1());
1530       for (const QByteArray& id : ids) {
1531         if (!id.isEmpty()) {
1532           it = searchByName(QString::fromLatin1(id));
1533           if (it != cend()) {
1534             break;
1535           }
1536         }
1537       }
1538     }
1539   }
1540   if (index > 0 && it != cend()) {
1541     const Frame::ExtendedType extendedType = it->getExtendedType();
1542     for (int i = 0; i < index && it != cend(); ++i, ++it) {}
1543     if (it != cend() && !(it->getExtendedType() == extendedType)) {
1544       it = cend();
1545     }
1546   }
1547   return it;
1548 }
1549 
1550 /**
1551  * Find a frame by type or name.
1552  *
1553  * @param type  type and name of the frame to find, if the exact name is not
1554  *              found, a case-insensitive search for the first name
1555  *              starting with this string is performed
1556  * @param index 0 for first frame with @a type, 1 for second, etc.
1557  *
1558  * @return iterator or end() if not found.
1559  */
1560 FrameCollection::const_iterator FrameCollection::findByExtendedType(
1561     const Frame::ExtendedType& type, int index) const
1562 {
1563   Frame frame(type, QLatin1String(""), -1);
1564   auto it = find(frame);
1565   if (it == cend()) {
1566     it = searchByName(frame.getInternalName());
1567   }
1568   if (index > 0 && it != cend()) {
1569     const Frame::ExtendedType extendedType = it->getExtendedType();
1570     for (int i = 0; i < index && it != cend(); ++i, ++it) {}
1571     if (it != cend() && !(it->getExtendedType() == extendedType)) {
1572       it = cend();
1573     }
1574   }
1575   return it;
1576 }
1577 
1578 /**
1579  * Find a frame by index.
1580  *
1581  * @param index the index in the frame, see \ref Frame::getIndex()
1582  *
1583  * @return iterator or end() if not found.
1584  */
1585 FrameCollection::const_iterator FrameCollection::findByIndex(int index) const
1586 {
1587   const_iterator it;
1588   for (it = cbegin(); it != cend(); ++it) {
1589     if (it->getIndex() == index) {
1590       break;
1591     }
1592   }
1593   return it;
1594 }
1595 
1596 /**
1597  * Get value by type.
1598  *
1599  * @param type type
1600  *
1601  * @return value, QString::null if not found.
1602  */
1603 QString FrameCollection::getValue(Frame::Type type) const
1604 {
1605   Frame frame(type, QLatin1String(""), QLatin1String(""), -1);
1606   auto it = find(frame);
1607   return it != cend() ? it->getValue() : QString();
1608 }
1609 
1610 /**
1611  * Get value by type and name.
1612  *
1613  * @param type  type and name of the frame to find, if the exact name is not
1614  *              found, a case-insensitive search for the first name
1615  *              starting with this string is performed
1616  *
1617  * @return value, QString::null if not found.
1618  */
1619 QString FrameCollection::getValue(const Frame::ExtendedType& type) const
1620 {
1621   auto it = findByExtendedType(type);
1622   return it != cend() ? it->getValue() : QString();
1623 }
1624 
1625 /**
1626  * Set value by type.
1627  *
1628  * @param type type
1629  * @param value value, nothing is done if QString::null
1630  */
1631 void FrameCollection::setValue(Frame::Type type, const QString& value)
1632 {
1633   if (!value.isNull()) {
1634     Frame frame(type, QLatin1String(""), QLatin1String(""), -1);
1635     if (auto it = find(frame); it != end()) {
1636       auto& frameFound = const_cast<Frame&>(*it);
1637       frameFound.setValueIfChanged(value);
1638     } else {
1639       frame.setValueIfChanged(value);
1640       insert(frame);
1641     }
1642   }
1643 }
1644 
1645 /**
1646  * Set value by type and name.
1647  *
1648  * @param type  type and name of the frame to find, if the exact name is not
1649  *              found, a case-insensitive search for the first name
1650  *              starting with this string is performed
1651  * @param value value, nothing is done if QString::null
1652  */
1653 void FrameCollection::setValue(const Frame::ExtendedType& type,
1654                                const QString& value)
1655 {
1656   if (!value.isNull()) {
1657     Frame frame(type, QLatin1String(""), -1);
1658     auto it = find(frame);
1659     if (it == end()) {
1660       it = searchByName(type.getInternalName());
1661     }
1662     if (it != end()) {
1663       auto& frameFound = const_cast<Frame&>(*it);
1664       frameFound.setValueIfChanged(value);
1665     } else {
1666       frame.setValueIfChanged(value);
1667       insert(frame);
1668     }
1669   }
1670 }
1671 
1672 /**
1673  * Get integer value by type.
1674  *
1675  * @param type type
1676  *
1677  * @return value, 0 if empty, -1 if not found.
1678  */
1679 int FrameCollection::getIntValue(Frame::Type type) const
1680 {
1681   QString str = getValue(type);
1682   return str.isNull() ? -1 : str.toInt();
1683 }
1684 
1685 /**
1686  * Set integer value by type.
1687  *
1688  * @param type type
1689  * @param value value, 0 to set empty, nothing is done if -1
1690  */
1691 void FrameCollection::setIntValue(Frame::Type type, int value)
1692 {
1693   if (value != -1) {
1694     QString str = value != 0 ? QString::number(value) : QLatin1String("");
1695     setValue(type, str);
1696   }
1697 }
1698 
1699 /**
1700  * Compare the frames with another frame collection and mark the value as
1701  * changed on frames which are different.
1702  *
1703  * @param other other frame collection
1704  */
1705 void FrameCollection::markChangedFrames(const FrameCollection& other)
1706 {
1707   for (auto it = begin(); it != end(); ++it) {
1708     auto otherIt = it->getIndex() != -1
1709         ? other.findByIndex(it->getIndex())
1710         : other.find(*it);
1711     auto& frame = const_cast<Frame&>(*it);
1712     frame.setValueChanged(!(otherIt != other.cend() && otherIt->isEqual(*it)));
1713   }
1714 }
1715 
1716 #ifndef QT_NO_DEBUG
1717 /**
1718  * Dump contents of frame collection to debug console.
1719  */
1720 void FrameCollection::dump() const
1721 {
1722   qDebug("FrameCollection:");
1723   for (auto it = cbegin(); it != cend(); ++it) {
1724     it->dump();
1725   }
1726 }
1727 #endif
1728 
1729 /**
1730  * Create a frame collection from a list of subframe fields.
1731  *
1732  * The given subframe fields must start with a Frame::ID_Subframe field with
1733  * the frame name as its value, followed by the fields of the frame. More
1734  * subframes may follow.
1735  *
1736  * @param begin iterator to begin of subframes
1737  * @param end iterator after end of subframes
1738  *
1739  * @return frames constructed from subframe fields.
1740  */
1741 FrameCollection FrameCollection::fromSubframes(
1742     Frame::FieldList::const_iterator begin, // clazy:exclude=function-args-by-ref
1743     Frame::FieldList::const_iterator end) // clazy:exclude=function-args-by-ref
1744 {
1745   FrameCollection frames;
1746   Frame frame;
1747   int index = 0;
1748   for (auto it = begin; it != end; ++it) {
1749     if (const Frame::Field& fld = *it; fld.m_id == Frame::ID_Subframe) {
1750       if (frame.getType() != Frame::FT_UnknownFrame) {
1751         frame.setValueFromFieldList();
1752         frames.insert(frame);
1753         frame = Frame();
1754       }
1755       if (QString name = fld.m_value.toString(); !name.isEmpty()) {
1756         frame.setExtendedType(Frame::ExtendedType(name));
1757         frame.setIndex(index++);
1758       }
1759     } else {
1760       if (frame.getType() != Frame::FT_UnknownFrame) {
1761         frame.fieldList().append(fld);
1762       }
1763     }
1764   }
1765   if (frame.getType() != Frame::FT_UnknownFrame) {
1766     frame.setValueFromFieldList();
1767     frames.insert(frame);
1768   }
1769   return frames;
1770 }
1771 
1772 
1773 /**
1774  * Constructor.
1775  * All frames are disabled
1776  */
1777 FrameFilter::FrameFilter() : m_enabledFrames(0) {}
1778 
1779 /**
1780  * Enable all frames.
1781  */
1782 void FrameFilter::enableAll()
1783 {
1784   m_enabledFrames = FTM_AllFrames;
1785   m_disabledOtherFrames.clear();
1786 }
1787 
1788 /**
1789  * Check if all fields are true.
1790  *
1791  * @return true if all fields are true.
1792  */
1793 bool FrameFilter::areAllEnabled() const
1794 {
1795   return (m_enabledFrames & FTM_AllFrames) == FTM_AllFrames &&
1796     m_disabledOtherFrames.empty();
1797 }
1798 
1799 /**
1800  * Check if frame is enabled.
1801  *
1802  * @param type frame type
1803  * @param name frame name
1804  *
1805  * @return true if frame is enabled.
1806  */
1807 bool FrameFilter::isEnabled(Frame::Type type, const QString& name) const
1808 {
1809   if (type <= Frame::FT_LastFrame) {
1810     return (m_enabledFrames & (1ULL << type)) != 0;
1811   }
1812   if (!name.isEmpty()) {
1813     auto it = m_disabledOtherFrames.find(name);
1814     return it == m_disabledOtherFrames.end();
1815   }
1816   return true;
1817 }
1818 
1819 /**
1820  * Enable or disable frame.
1821  *
1822  * @param type frame type
1823  * @param name frame name
1824  * @param en true to enable
1825  */
1826 void FrameFilter::enable(Frame::Type type, const QString& name, bool en)
1827 {
1828   if (type <= Frame::FT_LastFrame) {
1829     if (en) {
1830       m_enabledFrames |= (1ULL << type);
1831     } else {
1832       m_enabledFrames &= ~(1ULL << type);
1833     }
1834   } else if (!name.isEmpty()) {
1835     if (en) {
1836       if (auto it = m_disabledOtherFrames.find(name);
1837           it != m_disabledOtherFrames.end()) {
1838         m_disabledOtherFrames.erase(it);
1839       }
1840     } else {
1841       m_disabledOtherFrames.insert(name);
1842     }
1843   }
1844 }
1845 
1846 
1847 /**
1848  * Constructor.
1849  *
1850  * @param frames frame collection
1851  * @param str    string with format codes
1852  */
1853 FrameFormatReplacer::FrameFormatReplacer(
1854   const FrameCollection& frames, const QString& str)
1855   : FormatReplacer(str), m_frames(frames) {}
1856 
1857 /**
1858  * Replace a format code (one character %c or multiple characters %{chars}).
1859  * Supported format fields:
1860  * %s title (song)
1861  * %l album
1862  * %a artist
1863  * %c comment
1864  * %y year
1865  * %t track, two digits, i.e. leading zero if < 10
1866  * %T track, without leading zeroes
1867  * %g genre
1868  *
1869  * @param code format code
1870  *
1871  * @return replacement string,
1872  *         QString::null if code not found.
1873  */
1874 QString FrameFormatReplacer::getReplacement(const QString& code) const
1875 {
1876   QString result;
1877   QString name;
1878 
1879   if (code.length() == 1) {
1880     static const struct {
1881       const char* longCode;
1882       char shortCode;
1883     } shortToLong[] = {
1884       { "title", 's' },
1885       { "album", 'l' },
1886       { "artist", 'a' },
1887       { "comment", 'c' },
1888       { "year", 'y' },
1889       { "track", 't' },
1890       { "tracknumber", 'T' },
1891       { "genre", 'g' }
1892     };
1893     const char c = code[0].toLatin1();
1894     for (const auto& [longCode, shortCode] : shortToLong) {
1895       if (shortCode == c) {
1896         name = QString::fromLatin1(longCode);
1897         break;
1898       }
1899     }
1900   } else if (code.length() > 1) {
1901     name = code;
1902   }
1903 
1904   if (!name.isNull()) {
1905     QString lcName(name.toLower());
1906     QString fieldName;
1907     int fieldWidth = lcName == QLatin1String("track") ? 2 : -1;
1908     if (lcName == QLatin1String("year")) {
1909       name = QLatin1String("date");
1910     } else if (lcName == QLatin1String("tracknumber")) {
1911       name = QLatin1String("track number");
1912     }
1913     if (int len = lcName.length();
1914         len > 2 && lcName.at(len - 2) == QLatin1Char('.') &&
1915         lcName.at(len - 1) >= QLatin1Char('0') &&
1916         lcName.at(len - 1) <= QLatin1Char('9')) {
1917       fieldWidth = lcName.at(len - 1).toLatin1() - '0';
1918       lcName.truncate(len - 2);
1919       name.truncate(len - 2);
1920     }
1921     if (const int dotIndex = name.indexOf(QLatin1Char('.')); dotIndex != -1) {
1922       fieldName = name.mid(dotIndex + 1);
1923       name.truncate(dotIndex);
1924     }
1925 
1926     if (name == QLatin1String("disk")) {
1927       name = QLatin1String("disc number");
1928     }
1929 
1930     if (auto it = m_frames.findByName(name); it != m_frames.cend()) {
1931       if (fieldName.isEmpty()) {
1932         result = it->getValue().trimmed();
1933       } else {
1934         result = Frame::getField(*it, fieldName).toString().trimmed();
1935       }
1936       if (result.isNull()) {
1937         // code was found, but value is empty
1938         result = QLatin1String("");
1939       }
1940       if (it->getType() == Frame::FT_Picture && result.isEmpty()) {
1941         if (QVariant fieldValue = it->getFieldValue(Frame::ID_Data);
1942             fieldValue.isValid() && fieldValue.toByteArray().size() > 0) {
1943           // If there is a picture without description, return "1", so that
1944           // an empty value indicates "no picture"
1945           result = QLatin1String("1");
1946         }
1947       }
1948     }
1949 
1950     if (lcName == QLatin1String("year")) {
1951       QRegularExpression yearRe(QLatin1String("^\\d{4}-\\d{2}"));
1952       if (auto match = yearRe.match(result); match.hasMatch()) {
1953         result.truncate(4);
1954       }
1955     }
1956 
1957     if (fieldWidth > 0) {
1958       bool ok;
1959       int nr = Frame::numberWithoutTotal(result, &ok);
1960       if (ok) {
1961         result = QString(QLatin1String("%1"))
1962             .arg(nr, fieldWidth, 10, QLatin1Char('0'));
1963       }
1964     }
1965   }
1966 
1967   return result;
1968 }
1969 
1970 /**
1971  * Get help text for supported format codes.
1972  *
1973  * @param onlyRows if true only the tr elements are returned,
1974  *                 not the surrounding table
1975  *
1976  * @return help text.
1977  */
1978 QString FrameFormatReplacer::getToolTip(bool onlyRows)
1979 {
1980   QString str;
1981   if (!onlyRows) str += QLatin1String("<table>\n");
1982 
1983   str += QLatin1String("<tr><td>%s</td><td>%{title}</td><td>");
1984   str += QCoreApplication::translate("@default", "Title");
1985   str += QLatin1String("</td></tr>\n");
1986 
1987   str += QLatin1String("<tr><td>%l</td><td>%{album}</td><td>");
1988   str += QCoreApplication::translate("@default", "Album");
1989   str += QLatin1String("</td></tr>\n");
1990 
1991   str += QLatin1String("<tr><td>%a</td><td>%{artist}</td><td>");
1992   str += QCoreApplication::translate("@default", "Artist");
1993   str += QLatin1String("</td></tr>\n");
1994 
1995   str += QLatin1String("<tr><td>%c</td><td>%{comment}</td><td>");
1996   str += QCoreApplication::translate("@default", "Comment");
1997   str += QLatin1String("</td></tr>\n");
1998 
1999   str += QLatin1String("<tr><td>%y</td><td>%{year}</td><td>");
2000   const char* const yearStr = QT_TRANSLATE_NOOP("@default", "Year");
2001   str += QCoreApplication::translate("@default", yearStr);
2002   str += QLatin1String("</td></tr>\n");
2003 
2004   str += QLatin1String("<tr><td>%t</td><td>%{track}</td><td>");
2005   const char* const trackStr = QT_TRANSLATE_NOOP("@default", "Track");
2006   str += QCoreApplication::translate("@default", trackStr);
2007   str += QLatin1String(" &quot;01&quot;</td></tr>\n");
2008 
2009   str += QLatin1String("<tr><td>%t</td><td>%{track.3}</td><td>");
2010   str += QCoreApplication::translate("@default", trackStr);
2011   str += QLatin1String(" &quot;001&quot;</td></tr>\n");
2012 
2013   str += QLatin1String("<tr><td>%T</td><td>%{tracknumber}</td><td>");
2014   str += QCoreApplication::translate("@default", trackStr);
2015   str += QLatin1String(" &quot;1&quot;</td></tr>\n");
2016 
2017   str += QLatin1String("<tr><td>%g</td><td>%{genre}</td><td>");
2018   str += QCoreApplication::translate("@default", "Genre");
2019   str += QLatin1String("</td></tr>\n");
2020 
2021   str += QLatin1String(R"(<tr><td></td><td>%{"t1"title"t2"}...</td><td>)");
2022   const char* const prependAppendStr =
2023       QT_TRANSLATE_NOOP("@default", "Prepend t1/append t2 if not empty");
2024   str += QCoreApplication::translate("@default", prependAppendStr);
2025   str += QLatin1String("</td></tr>\n");
2026 
2027   if (!onlyRows) str += QLatin1String("</table>\n");
2028   return str;
2029 }