diff --git a/taglib/mpeg/id3v2/frames/textidentificationframe.cpp b/taglib/mpeg/id3v2/frames/textidentificationframe.cpp index 409cbbdd..9ead4d62 100644 --- a/taglib/mpeg/id3v2/frames/textidentificationframe.cpp +++ b/taglib/mpeg/id3v2/frames/textidentificationframe.cpp @@ -135,6 +135,21 @@ namespace std::pair("DJ-MIX", "DJMIXER"), std::pair("MIX", "MIXER"), }; + + constexpr std::array txxxFrameTranslation { + std::pair("MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID"), + std::pair("MUSICBRAINZ ARTIST ID", "MUSICBRAINZ_ARTISTID"), + std::pair("MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ_ALBUMARTISTID"), + std::pair("MUSICBRAINZ ALBUM RELEASE COUNTRY", "RELEASECOUNTRY"), + std::pair("MUSICBRAINZ ALBUM STATUS", "RELEASESTATUS"), + std::pair("MUSICBRAINZ ALBUM TYPE", "RELEASETYPE"), + std::pair("MUSICBRAINZ RELEASE GROUP ID", "MUSICBRAINZ_RELEASEGROUPID"), + std::pair("MUSICBRAINZ RELEASE TRACK ID", "MUSICBRAINZ_RELEASETRACKID"), + std::pair("MUSICBRAINZ WORK ID", "MUSICBRAINZ_WORKID"), + std::pair("ACOUSTID ID", "ACOUSTID_ID"), + std::pair("ACOUSTID FINGERPRINT", "ACOUSTID_FINGERPRINT"), + std::pair("MUSICIP PUID", "MUSICIP_PUID"), + }; } // namespace const KeyConversionMap &TextIdentificationFrame::involvedPeopleMap() // static @@ -432,6 +447,26 @@ UserTextIdentificationFrame *UserTextIdentificationFrame::find( return nullptr; } +String UserTextIdentificationFrame::txxxToKey(const String &description) +{ + const String d = description.upper(); + for(const auto &[o, t] : txxxFrameTranslation) { + if(d == o) + return t; + } + return d; +} + +String UserTextIdentificationFrame::keyToTXXX(const String &s) +{ + const String key = s.upper(); + for(const auto &[o, t] : txxxFrameTranslation) { + if(key == t) + return o; + } + return s; +} + //////////////////////////////////////////////////////////////////////////////// // UserTextIdentificationFrame private members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/mpeg/id3v2/frames/textidentificationframe.h b/taglib/mpeg/id3v2/frames/textidentificationframe.h index 9b53fb83..e6c8755d 100644 --- a/taglib/mpeg/id3v2/frames/textidentificationframe.h +++ b/taglib/mpeg/id3v2/frames/textidentificationframe.h @@ -301,6 +301,16 @@ namespace TagLib { */ static UserTextIdentificationFrame *find(Tag *tag, const String &description); + /*! + * Returns an appropriate TXXX frame description for the given free-form tag key. + */ + static String keyToTXXX(const String &); + + /*! + * Returns a free-form tag name for the given ID3 frame description. + */ + static String txxxToKey(const String &); + private: UserTextIdentificationFrame(const ByteVector &data, Header *h); UserTextIdentificationFrame(const TextIdentificationFrame &); diff --git a/taglib/mpeg/id3v2/id3v2frame.cpp b/taglib/mpeg/id3v2/id3v2frame.cpp index 6c37a6a6..04b45711 100644 --- a/taglib/mpeg/id3v2/id3v2frame.cpp +++ b/taglib/mpeg/id3v2/id3v2frame.cpp @@ -97,56 +97,6 @@ unsigned int Frame::headerSize() return d->header->size(); } -Frame *Frame::createTextualFrame(const String &key, const StringList &values) //static -{ - // check if the key is contained in the key<=>frameID mapping - ByteVector frameID = keyToFrameID(key); - if(!frameID.isEmpty()) { - // Apple proprietary WFED (Podcast URL), MVNM (Movement Name), MVIN (Movement Number), GRP1 (Grouping) are in fact text frames. - if(frameID[0] == 'T' || frameID == "WFED" || frameID == "MVNM" || frameID == "MVIN" || frameID == "GRP1"){ // text frame - auto frame = new TextIdentificationFrame(frameID, String::UTF8); - frame->setText(values); - return frame; - } if((frameID[0] == 'W') && (values.size() == 1)){ // URL frame (not WXXX); support only one value - auto frame = new UrlLinkFrame(frameID); - frame->setUrl(values.front()); - return frame; - } if(frameID == "PCST") { - return new PodcastFrame(); - } - } - if(key == "MUSICBRAINZ_TRACKID" && values.size() == 1) { - auto frame = new UniqueFileIdentifierFrame("http://musicbrainz.org", values.front().data(String::UTF8)); - return frame; - } - // now we check if it's one of the "special" cases: - // -LYRICS: depending on the number of values, use USLT or TXXX (with description=LYRICS) - if((key == "LYRICS" || key.startsWith(lyricsPrefix)) && values.size() == 1){ - auto frame = new UnsynchronizedLyricsFrame(String::UTF8); - frame->setDescription(key == "LYRICS" ? key : key.substr(lyricsPrefix.size())); - frame->setText(values.front()); - return frame; - } - // -URL: depending on the number of values, use WXXX or TXXX (with description=URL) - if((key == "URL" || key.startsWith(urlPrefix)) && values.size() == 1){ - auto frame = new UserUrlLinkFrame(String::UTF8); - frame->setDescription(key == "URL" ? key : key.substr(urlPrefix.size())); - frame->setUrl(values.front()); - return frame; - } - // -COMMENT: depending on the number of values, use COMM or TXXX (with description=COMMENT) - if((key == "COMMENT" || key.startsWith(commentPrefix)) && values.size() == 1){ - auto frame = new CommentsFrame(String::UTF8); - if (key != "COMMENT"){ - frame->setDescription(key.substr(commentPrefix.size())); - } - frame->setText(values.front()); - return frame; - } - // if none of the above cases apply, we use a TXXX frame with the key as description - return new UserTextIdentificationFrame(keyToTXXX(key), values, String::UTF8); -} - Frame::~Frame() = default; ByteVector Frame::frameID() const @@ -181,6 +131,125 @@ ByteVector Frame::render() const return headerData + fieldData; } +Frame::Header *Frame::header() const +{ + return d->header; +} + +namespace +{ + constexpr std::array frameTranslation { + // Text information frames + std::pair("TALB", "ALBUM"), + std::pair("TBPM", "BPM"), + std::pair("TCOM", "COMPOSER"), + std::pair("TCON", "GENRE"), + std::pair("TCOP", "COPYRIGHT"), + std::pair("TDEN", "ENCODINGTIME"), + std::pair("TDLY", "PLAYLISTDELAY"), + std::pair("TDOR", "ORIGINALDATE"), + std::pair("TDRC", "DATE"), + // std::pair("TRDA", "DATE"), // id3 v2.3, replaced by TDRC in v2.4 + // std::pair("TDAT", "DATE"), // id3 v2.3, replaced by TDRC in v2.4 + // std::pair("TYER", "DATE"), // id3 v2.3, replaced by TDRC in v2.4 + // std::pair("TIME", "DATE"), // id3 v2.3, replaced by TDRC in v2.4 + std::pair("TDRL", "RELEASEDATE"), + std::pair("TDTG", "TAGGINGDATE"), + std::pair("TENC", "ENCODEDBY"), + std::pair("TEXT", "LYRICIST"), + std::pair("TFLT", "FILETYPE"), + // std::pair("TIPL", "INVOLVEDPEOPLE"), handled separately + std::pair("TIT1", "WORK"), // 'Work' in iTunes + std::pair("TIT2", "TITLE"), + std::pair("TIT3", "SUBTITLE"), + std::pair("TKEY", "INITIALKEY"), + std::pair("TLAN", "LANGUAGE"), + std::pair("TLEN", "LENGTH"), + // std::pair("TMCL", "MUSICIANCREDITS"), handled separately + std::pair("TMED", "MEDIA"), + std::pair("TMOO", "MOOD"), + std::pair("TOAL", "ORIGINALALBUM"), + std::pair("TOFN", "ORIGINALFILENAME"), + std::pair("TOLY", "ORIGINALLYRICIST"), + std::pair("TOPE", "ORIGINALARTIST"), + std::pair("TOWN", "OWNER"), + std::pair("TPE1", "ARTIST"), + std::pair("TPE2", "ALBUMARTIST"), // id3's spec says 'PERFORMER', but most programs use 'ALBUMARTIST' + std::pair("TPE3", "CONDUCTOR"), + std::pair("TPE4", "REMIXER"), // could also be ARRANGER + std::pair("TPOS", "DISCNUMBER"), + std::pair("TPRO", "PRODUCEDNOTICE"), + std::pair("TPUB", "LABEL"), + std::pair("TRCK", "TRACKNUMBER"), + std::pair("TRSN", "RADIOSTATION"), + std::pair("TRSO", "RADIOSTATIONOWNER"), + std::pair("TSOA", "ALBUMSORT"), + std::pair("TSOC", "COMPOSERSORT"), + std::pair("TSOP", "ARTISTSORT"), + std::pair("TSOT", "TITLESORT"), + std::pair("TSO2", "ALBUMARTISTSORT"), // non-standard, used by iTunes + std::pair("TSRC", "ISRC"), + std::pair("TSSE", "ENCODING"), + std::pair("TSST", "DISCSUBTITLE"), + // URL frames + std::pair("WCOP", "COPYRIGHTURL"), + std::pair("WOAF", "FILEWEBPAGE"), + std::pair("WOAR", "ARTISTWEBPAGE"), + std::pair("WOAS", "AUDIOSOURCEWEBPAGE"), + std::pair("WORS", "RADIOSTATIONWEBPAGE"), + std::pair("WPAY", "PAYMENTWEBPAGE"), + std::pair("WPUB", "PUBLISHERWEBPAGE"), + // std::pair("WXXX", "URL"), handled specially + // Other frames + std::pair("COMM", "COMMENT"), + // std::pair("USLT", "LYRICS"), handled specially + // Apple iTunes proprietary frames + std::pair("PCST", "PODCAST"), + std::pair("TCAT", "PODCASTCATEGORY"), + std::pair("TDES", "PODCASTDESC"), + std::pair("TGID", "PODCASTID"), + std::pair("WFED", "PODCASTURL"), + std::pair("MVNM", "MOVEMENTNAME"), + std::pair("MVIN", "MOVEMENTNUMBER"), + std::pair("GRP1", "GROUPING"), + std::pair("TCMP", "COMPILATION"), + }; + + // list of deprecated frames and their successors + constexpr std::array deprecatedFrames { + std::pair("TRDA", "TDRC"), // 2.3 -> 2.4 (http://en.wikipedia.org/wiki/ID3) + std::pair("TDAT", "TDRC"), // 2.3 -> 2.4 + std::pair("TYER", "TDRC"), // 2.3 -> 2.4 + std::pair("TIME", "TDRC"), // 2.3 -> 2.4 + }; +} // namespace + +String Frame::frameIDToKey(const ByteVector &id) +{ + ByteVector id24 = id; + for(const auto &[o, t] : deprecatedFrames) { + if(id24 == o) { + id24 = t; + break; + } + } + for(const auto &[o, t] : frameTranslation) { + if(id24 == o) + return t; + } + return String(); +} + +ByteVector Frame::keyToFrameID(const String &s) +{ + const String key = s.upper(); + for(const auto &[o, t] : frameTranslation) { + if(key == t) + return o; + } + return ByteVector(); +} + //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// @@ -197,11 +266,6 @@ Frame::Frame(Header *h) : d->header = h; } -Frame::Header *Frame::header() const -{ - return d->header; -} - void Frame::setHeader(Header *h, bool deleteCurrent) { if(deleteCurrent) @@ -296,155 +360,6 @@ String::Type Frame::checkTextEncoding(const StringList &fields, String::Type enc return String::Latin1; } -namespace -{ - constexpr std::array frameTranslation { - // Text information frames - std::pair("TALB", "ALBUM"), - std::pair("TBPM", "BPM"), - std::pair("TCOM", "COMPOSER"), - std::pair("TCON", "GENRE"), - std::pair("TCOP", "COPYRIGHT"), - std::pair("TDEN", "ENCODINGTIME"), - std::pair("TDLY", "PLAYLISTDELAY"), - std::pair("TDOR", "ORIGINALDATE"), - std::pair("TDRC", "DATE"), - // std::pair("TRDA", "DATE"), // id3 v2.3, replaced by TDRC in v2.4 - // std::pair("TDAT", "DATE"), // id3 v2.3, replaced by TDRC in v2.4 - // std::pair("TYER", "DATE"), // id3 v2.3, replaced by TDRC in v2.4 - // std::pair("TIME", "DATE"), // id3 v2.3, replaced by TDRC in v2.4 - std::pair("TDRL", "RELEASEDATE"), - std::pair("TDTG", "TAGGINGDATE"), - std::pair("TENC", "ENCODEDBY"), - std::pair("TEXT", "LYRICIST"), - std::pair("TFLT", "FILETYPE"), - // std::pair("TIPL", "INVOLVEDPEOPLE"), handled separately - std::pair("TIT1", "WORK"), // 'Work' in iTunes - std::pair("TIT2", "TITLE"), - std::pair("TIT3", "SUBTITLE"), - std::pair("TKEY", "INITIALKEY"), - std::pair("TLAN", "LANGUAGE"), - std::pair("TLEN", "LENGTH"), - // std::pair("TMCL", "MUSICIANCREDITS"), handled separately - std::pair("TMED", "MEDIA"), - std::pair("TMOO", "MOOD"), - std::pair("TOAL", "ORIGINALALBUM"), - std::pair("TOFN", "ORIGINALFILENAME"), - std::pair("TOLY", "ORIGINALLYRICIST"), - std::pair("TOPE", "ORIGINALARTIST"), - std::pair("TOWN", "OWNER"), - std::pair("TPE1", "ARTIST"), - std::pair("TPE2", "ALBUMARTIST"), // id3's spec says 'PERFORMER', but most programs use 'ALBUMARTIST' - std::pair("TPE3", "CONDUCTOR"), - std::pair("TPE4", "REMIXER"), // could also be ARRANGER - std::pair("TPOS", "DISCNUMBER"), - std::pair("TPRO", "PRODUCEDNOTICE"), - std::pair("TPUB", "LABEL"), - std::pair("TRCK", "TRACKNUMBER"), - std::pair("TRSN", "RADIOSTATION"), - std::pair("TRSO", "RADIOSTATIONOWNER"), - std::pair("TSOA", "ALBUMSORT"), - std::pair("TSOC", "COMPOSERSORT"), - std::pair("TSOP", "ARTISTSORT"), - std::pair("TSOT", "TITLESORT"), - std::pair("TSO2", "ALBUMARTISTSORT"), // non-standard, used by iTunes - std::pair("TSRC", "ISRC"), - std::pair("TSSE", "ENCODING"), - std::pair("TSST", "DISCSUBTITLE"), - // URL frames - std::pair("WCOP", "COPYRIGHTURL"), - std::pair("WOAF", "FILEWEBPAGE"), - std::pair("WOAR", "ARTISTWEBPAGE"), - std::pair("WOAS", "AUDIOSOURCEWEBPAGE"), - std::pair("WORS", "RADIOSTATIONWEBPAGE"), - std::pair("WPAY", "PAYMENTWEBPAGE"), - std::pair("WPUB", "PUBLISHERWEBPAGE"), - // std::pair("WXXX", "URL"), handled specially - // Other frames - std::pair("COMM", "COMMENT"), - // std::pair("USLT", "LYRICS"), handled specially - // Apple iTunes proprietary frames - std::pair("PCST", "PODCAST"), - std::pair("TCAT", "PODCASTCATEGORY"), - std::pair("TDES", "PODCASTDESC"), - std::pair("TGID", "PODCASTID"), - std::pair("WFED", "PODCASTURL"), - std::pair("MVNM", "MOVEMENTNAME"), - std::pair("MVIN", "MOVEMENTNUMBER"), - std::pair("GRP1", "GROUPING"), - std::pair("TCMP", "COMPILATION"), - }; - - constexpr std::array txxxFrameTranslation { - std::pair("MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID"), - std::pair("MUSICBRAINZ ARTIST ID", "MUSICBRAINZ_ARTISTID"), - std::pair("MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ_ALBUMARTISTID"), - std::pair("MUSICBRAINZ ALBUM RELEASE COUNTRY", "RELEASECOUNTRY"), - std::pair("MUSICBRAINZ ALBUM STATUS", "RELEASESTATUS"), - std::pair("MUSICBRAINZ ALBUM TYPE", "RELEASETYPE"), - std::pair("MUSICBRAINZ RELEASE GROUP ID", "MUSICBRAINZ_RELEASEGROUPID"), - std::pair("MUSICBRAINZ RELEASE TRACK ID", "MUSICBRAINZ_RELEASETRACKID"), - std::pair("MUSICBRAINZ WORK ID", "MUSICBRAINZ_WORKID"), - std::pair("ACOUSTID ID", "ACOUSTID_ID"), - std::pair("ACOUSTID FINGERPRINT", "ACOUSTID_FINGERPRINT"), - std::pair("MUSICIP PUID", "MUSICIP_PUID"), - }; - - // list of deprecated frames and their successors - constexpr std::array deprecatedFrames { - std::pair("TRDA", "TDRC"), // 2.3 -> 2.4 (http://en.wikipedia.org/wiki/ID3) - std::pair("TDAT", "TDRC"), // 2.3 -> 2.4 - std::pair("TYER", "TDRC"), // 2.3 -> 2.4 - std::pair("TIME", "TDRC"), // 2.3 -> 2.4 - }; -} // namespace - -String Frame::frameIDToKey(const ByteVector &id) -{ - ByteVector id24 = id; - for(const auto &[o, t] : deprecatedFrames) { - if(id24 == o) { - id24 = t; - break; - } - } - for(const auto &[o, t] : frameTranslation) { - if(id24 == o) - return t; - } - return String(); -} - -ByteVector Frame::keyToFrameID(const String &s) -{ - const String key = s.upper(); - for(const auto &[o, t] : frameTranslation) { - if(key == t) - return o; - } - return ByteVector(); -} - -String Frame::txxxToKey(const String &description) -{ - const String d = description.upper(); - for(const auto &[o, t] : txxxFrameTranslation) { - if(d == o) - return t; - } - return d; -} - -String Frame::keyToTXXX(const String &s) -{ - const String key = s.upper(); - for(const auto &[o, t] : txxxFrameTranslation) { - if(key == t) - return o; - } - return s; -} - PropertyMap Frame::asProperties() const { if(dynamic_cast< const UnknownFrame *>(this)) { diff --git a/taglib/mpeg/id3v2/id3v2frame.h b/taglib/mpeg/id3v2/id3v2frame.h index 0f090453..41743ff6 100644 --- a/taglib/mpeg/id3v2/id3v2frame.h +++ b/taglib/mpeg/id3v2/id3v2frame.h @@ -54,18 +54,9 @@ namespace TagLib { class TAGLIB_EXPORT Frame { friend class Tag; - friend class FrameFactory; - friend class TableOfContentsFrame; - friend class ChapterFrame; public: - - /*! - * Creates a textual frame which corresponds to a single key in the PropertyMap - * interface. These are all (User)TextIdentificationFrames except TIPL and TMCL, - * all (User)URLLinkFrames, CommentsFrames, and UnsynchronizedLyricsFrame. - */ - static Frame *createTextualFrame(const String &key, const StringList &values); + class Header; /*! * Destroys this Frame instance. @@ -122,12 +113,29 @@ namespace TagLib { */ ByteVector render() const; + /*! + * Returns a pointer to the frame header. + */ + Header *header() const; + /*! * Returns the text delimiter that is used between fields for the string * type \a t. */ static ByteVector textDelimiter(String::Type t); + /*! + * Returns an appropriate ID3 frame ID for the given free-form tag key. This method + * will return an empty ByteVector if no specialized translation is found. + */ + static ByteVector keyToFrameID(const String &); + + /*! + * Returns a free-form tag name for the given ID3 frame ID. Note that this does not work + * for general frame IDs such as TXXX or WXXX; in such a case an empty string is returned. + */ + static String frameIDToKey(const ByteVector &); + /*! * The string with which an instrument name is prefixed to build a key in a PropertyMap; * used to translate PropertyMaps to TMCL frames. In the current implementation, this @@ -151,8 +159,6 @@ namespace TagLib { static const String urlPrefix; protected: - class Header; - /*! * Constructs an ID3v2 frame using \a data to read the header information. * All other processing of \a data should be handled in a subclass. @@ -170,11 +176,6 @@ namespace TagLib { */ Frame(Header *h); - /*! - * Returns a pointer to the frame header. - */ - Header *header() const; - /*! * Sets the header to \a h. If \a deleteCurrent is true, this will free * the memory of the current header. @@ -236,28 +237,6 @@ namespace TagLib { */ virtual PropertyMap asProperties() const; - /*! - * Returns an appropriate ID3 frame ID for the given free-form tag key. This method - * will return an empty ByteVector if no specialized translation is found. - */ - static ByteVector keyToFrameID(const String &); - - /*! - * Returns a free-form tag name for the given ID3 frame ID. Note that this does not work - * for general frame IDs such as TXXX or WXXX; in such a case an empty string is returned. - */ - static String frameIDToKey(const ByteVector &); - - /*! - * Returns an appropriate TXXX frame description for the given free-form tag key. - */ - static String keyToTXXX(const String &); - - /*! - * Returns a free-form tag name for the given ID3 frame description. - */ - static String txxxToKey(const String &); - /*! * This helper function splits the PropertyMap \a original into three ProperytMaps * \a singleFrameProperties, \a tiplProperties, and \a tmclProperties, such that: diff --git a/taglib/mpeg/id3v2/id3v2framefactory.cpp b/taglib/mpeg/id3v2/id3v2framefactory.cpp index 5f692804..66f39509 100644 --- a/taglib/mpeg/id3v2/id3v2framefactory.cpp +++ b/taglib/mpeg/id3v2/id3v2framefactory.cpp @@ -370,6 +370,11 @@ void FrameFactory::setDefaultTextEncoding(String::Type encoding) d->defaultEncoding = encoding; } +bool FrameFactory::isUsingDefaultTextEncoding() const +{ + return d->useDefaultEncoding; +} + //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// @@ -538,3 +543,54 @@ bool FrameFactory::updateFrame(Frame::Header *header) const return true; } + +Frame *FrameFactory::createFrameForProperty(const String &key, const StringList &values) const +{ + // check if the key is contained in the key<=>frameID mapping + ByteVector frameID = Frame::keyToFrameID(key); + if(!frameID.isEmpty()) { + // Apple proprietary WFED (Podcast URL), MVNM (Movement Name), MVIN (Movement Number), GRP1 (Grouping) are in fact text frames. + if(frameID[0] == 'T' || frameID == "WFED" || frameID == "MVNM" || frameID == "MVIN" || frameID == "GRP1"){ // text frame + auto frame = new TextIdentificationFrame(frameID, String::UTF8); + frame->setText(values); + return frame; + } if((frameID[0] == 'W') && (values.size() == 1)){ // URL frame (not WXXX); support only one value + auto frame = new UrlLinkFrame(frameID); + frame->setUrl(values.front()); + return frame; + } if(frameID == "PCST") { + return new PodcastFrame(); + } + } + if(key == "MUSICBRAINZ_TRACKID" && values.size() == 1) { + auto frame = new UniqueFileIdentifierFrame("http://musicbrainz.org", values.front().data(String::UTF8)); + return frame; + } + // now we check if it's one of the "special" cases: + // -LYRICS: depending on the number of values, use USLT or TXXX (with description=LYRICS) + if((key == "LYRICS" || key.startsWith(Frame::lyricsPrefix)) && values.size() == 1){ + auto frame = new UnsynchronizedLyricsFrame(String::UTF8); + frame->setDescription(key == "LYRICS" ? key : key.substr(Frame::lyricsPrefix.size())); + frame->setText(values.front()); + return frame; + } + // -URL: depending on the number of values, use WXXX or TXXX (with description=URL) + if((key == "URL" || key.startsWith(Frame::urlPrefix)) && values.size() == 1){ + auto frame = new UserUrlLinkFrame(String::UTF8); + frame->setDescription(key == "URL" ? key : key.substr(Frame::urlPrefix.size())); + frame->setUrl(values.front()); + return frame; + } + // -COMMENT: depending on the number of values, use COMM or TXXX (with description=COMMENT) + if((key == "COMMENT" || key.startsWith(Frame::commentPrefix)) && values.size() == 1){ + auto frame = new CommentsFrame(String::UTF8); + if (key != "COMMENT"){ + frame->setDescription(key.substr(Frame::commentPrefix.size())); + } + frame->setText(values.front()); + return frame; + } + // if none of the above cases apply, we use a TXXX frame with the key as description + return new UserTextIdentificationFrame( + UserTextIdentificationFrame::keyToTXXX(key), values, String::UTF8); +} diff --git a/taglib/mpeg/id3v2/id3v2framefactory.h b/taglib/mpeg/id3v2/id3v2framefactory.h index 17bdc2a1..f41f60a6 100644 --- a/taglib/mpeg/id3v2/id3v2framefactory.h +++ b/taglib/mpeg/id3v2/id3v2framefactory.h @@ -50,10 +50,11 @@ namespace TagLib { * factory to be the default factory in ID3v2::Tag constructor you can * implement behavior that will allow for new ID3v2::Frame subclasses (also * provided by you) to be used. + * See \c testFrameFactory() in \e tests/test_mpeg.cpp for an example. * * This implements both abstract factory and singleton patterns * of which more information is available on the web and in software design - * textbooks (Notably Design Patters). + * textbooks (Notably Design Patterns). * * \note You do not need to use this factory to create new frames to add to * an ID3v2::Tag. You can instantiate frame subclasses directly (with new) @@ -76,6 +77,14 @@ namespace TagLib { */ virtual Frame *createFrame(const ByteVector &data, const Header *tagHeader) const; + /*! + * Creates a textual frame which corresponds to a single key in the + * PropertyMap interface. TIPL and TMCL do not belong to this category + * and are thus handled explicitly in the Frame class. + */ + virtual Frame *createFrameForProperty( + const String &key, const StringList &values) const; + /*! * After a tag has been read, this tries to rebuild some of them * information, most notably the recording date, from frames that @@ -105,6 +114,18 @@ namespace TagLib { */ void setDefaultTextEncoding(String::Type encoding); + /*! + * Returns true if defaultTextEncoding() is used. + * The default text encoding is used when setDefaultTextEncoding() has + * been called. In this case, reimplementations of FrameFactory should + * use defaultTextEncoding() on the frames (having a text encoding field) + * they create. + * + * \see defaultTextEncoding() + * \see setDefaultTextEncoding() + */ + bool isUsingDefaultTextEncoding() const; + protected: /*! * Constructs a frame factory. Because this is a singleton this method is diff --git a/taglib/mpeg/id3v2/id3v2tag.cpp b/taglib/mpeg/id3v2/id3v2tag.cpp index 689970ec..9fc10b4b 100644 --- a/taglib/mpeg/id3v2/id3v2tag.cpp +++ b/taglib/mpeg/id3v2/id3v2tag.cpp @@ -443,13 +443,13 @@ PropertyMap ID3v2::Tag::setProperties(const PropertyMap &origProps) // now create remaining frames: // start with the involved people list (TIPL) if(!tiplProperties.isEmpty()) - addFrame(TextIdentificationFrame::createTIPLFrame(tiplProperties)); + addFrame(TextIdentificationFrame::createTIPLFrame(tiplProperties)); // proceed with the musician credit list (TMCL) if(!tmclProperties.isEmpty()) - addFrame(TextIdentificationFrame::createTMCLFrame(tmclProperties)); + addFrame(TextIdentificationFrame::createTMCLFrame(tmclProperties)); // now create the "one key per frame" frames for(const auto &[tag, frames] : std::as_const(properties)) - addFrame(Frame::createTextualFrame(tag, frames)); + addFrame(d->factory->createFrameForProperty(tag, frames)); return PropertyMap(); // ID3 implements the complete PropertyMap interface, so an empty map is returned } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fbd6eb83..25e05979 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -47,6 +47,7 @@ SET(test_runner_SRCS test_fileref.cpp test_id3v1.cpp test_id3v2.cpp + test_id3v2framefactory.cpp test_xiphcomment.cpp test_aiff.cpp test_riff.cpp diff --git a/tests/test_id3v2framefactory.cpp b/tests/test_id3v2framefactory.cpp new file mode 100644 index 00000000..bdc4cfcb --- /dev/null +++ b/tests/test_id3v2framefactory.cpp @@ -0,0 +1,379 @@ +/*************************************************************************** + copyright : (C) 2023 by Urs Fleisch + email : ufleisch@users.sourceforge.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include +#include + +#include "tbytevector.h" +#include "tpropertymap.h" +#include "mpegfile.h" +#include "flacfile.h" +#include "trueaudiofile.h" +#include "wavfile.h" +#include "aifffile.h" +#include "dsffile.h" +#include "dsdifffile.h" +#include "id3v2tag.h" +#include "id3v2frame.h" +#include "id3v2framefactory.h" +#include +#include "utils.h" + +using namespace std; +using namespace TagLib; + +namespace +{ + + class CustomFrameFactory; + + // Just a silly example of a custom frame holding a number. + class CustomFrame : public ID3v2::Frame + { + friend class CustomFrameFactory; + public: + explicit CustomFrame(unsigned int value = 0) + : Frame("CUST"), m_value(value) {} + CustomFrame(const CustomFrame &) = delete; + CustomFrame &operator=(const CustomFrame &) = delete; + ~CustomFrame() override = default; + String toString() const override { return String::number(m_value); } + PropertyMap asProperties() const override { + return SimplePropertyMap{{"CUSTOM", StringList(String::number(m_value))}}; + } + unsigned int value() const { return m_value; } + + protected: + void parseFields(const ByteVector &data) override { + m_value = data.toUInt(); + } + ByteVector renderFields() const override { + return ByteVector::fromUInt(m_value); + } + + private: + CustomFrame(const ByteVector &data, Header *h) : Frame(h) { + parseFields(fieldData(data)); + } + unsigned int m_value; + }; + + // Example for frame factory with support for CustomFrame. + class CustomFrameFactory : public ID3v2::FrameFactory { + public: + ID3v2::Frame *createFrameForProperty( + const String &key, const StringList &values) const override { + if(key == "CUSTOM") { + return new CustomFrame(!values.isEmpty() ? values.front().toInt() : 0); + } + return ID3v2::FrameFactory::createFrameForProperty(key, values); + } + + protected: + ID3v2::Frame *createFrame(const ByteVector &data, ID3v2::Frame::Header *header, + const ID3v2::Header *tagHeader) const override { + if(header->frameID() == "CUST") { + return new CustomFrame(data, header); + } + return ID3v2::FrameFactory::createFrame(data, header, tagHeader); + } + }; + +} // namespace + +class TestId3v2FrameFactory : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestId3v2FrameFactory); + CPPUNIT_TEST(testMPEG); + CPPUNIT_TEST(testFLAC); + CPPUNIT_TEST(testTrueAudio); + CPPUNIT_TEST(testWAV); + CPPUNIT_TEST(testAIFF); + CPPUNIT_TEST(testDSF); + CPPUNIT_TEST(testDSDIFF); + CPPUNIT_TEST_SUITE_END(); + +public: + + void testGenericFrameFactory( + const char *fileName, + function createFileWithDefaultFactory, + function createFileWithFactory, + function hasID3v2Tag, + function getID3v2Tag, + function stripAllTags) + { + CustomFrameFactory factory; + + { + auto f = std::unique_ptr(createFileWithDefaultFactory(fileName)); + CPPUNIT_ASSERT(f->isValid()); + ID3v2::Tag *tag = getID3v2Tag(*f); + const ID3v2::FrameList frames = tag->frameList(); + for(const auto &frame : frames) { + tag->removeFrame(frame, false); + } + tag->setArtist("An artist"); + tag->setTitle("A title"); + f->save(); + } + { + auto f = std::unique_ptr(createFileWithDefaultFactory(fileName)); + CPPUNIT_ASSERT(f->isValid()); + CPPUNIT_ASSERT(hasID3v2Tag(*f)); + ID3v2::Tag *tag = getID3v2Tag(*f); + tag->addFrame(new CustomFrame(1234567890)); + f->save(); + } + { + auto f = std::unique_ptr(createFileWithDefaultFactory(fileName)); + CPPUNIT_ASSERT(f->isValid()); + CPPUNIT_ASSERT(hasID3v2Tag(*f)); + ID3v2::Tag *tag = getID3v2Tag(*f); + const auto &frames = tag->frameList("CUST"); + CPPUNIT_ASSERT(!frames.isEmpty()); + // Without a specialized FrameFactory, you can add custom frames, + // but your cannot parse them. + CPPUNIT_ASSERT(!dynamic_cast(frames.front())); + } + { + auto f = std::unique_ptr(createFileWithFactory(fileName, &factory)); + CPPUNIT_ASSERT(f->isValid()); + CPPUNIT_ASSERT(hasID3v2Tag(*f)); + ID3v2::Tag *tag = getID3v2Tag(*f); + const auto &frames = tag->frameList("CUST"); + CPPUNIT_ASSERT(!frames.isEmpty()); + auto frame = dynamic_cast(frames.front()); + CPPUNIT_ASSERT(frame); + CPPUNIT_ASSERT_EQUAL(1234567890U, frame->value()); + PropertyMap properties = tag->properties(); + CPPUNIT_ASSERT_EQUAL(StringList("1234567890"), + properties.value("CUSTOM")); + CPPUNIT_ASSERT_EQUAL(StringList("An artist"), + properties.value("ARTIST")); + CPPUNIT_ASSERT_EQUAL(StringList("A title"), + properties.value("TITLE")); + stripAllTags(*f); + } + { + auto f = std::unique_ptr(createFileWithFactory(fileName, &factory)); + CPPUNIT_ASSERT(f->isValid()); + CPPUNIT_ASSERT(!hasID3v2Tag(*f)); + ID3v2::Tag *tag = getID3v2Tag(*f); + PropertyMap properties = tag->properties(); + CPPUNIT_ASSERT(properties.isEmpty()); + properties.insert("CUSTOM", StringList("305419896")); + tag->setProperties(properties); + f->save(); + } + { + auto f = std::unique_ptr(createFileWithFactory(fileName, &factory)); + CPPUNIT_ASSERT(f->isValid()); + CPPUNIT_ASSERT(hasID3v2Tag(*f)); + ID3v2::Tag *tag = getID3v2Tag(*f); + PropertyMap properties = tag->properties(); + CPPUNIT_ASSERT_EQUAL(StringList("305419896"), properties.value("CUSTOM")); + const auto &frames = tag->frameList("CUST"); + CPPUNIT_ASSERT(!frames.isEmpty()); + auto frame = dynamic_cast(frames.front()); + CPPUNIT_ASSERT(frame); + CPPUNIT_ASSERT_EQUAL(0x12345678U, frame->value()); + } + } + + void testMPEG() + { + ScopedFileCopy copy("lame_cbr", ".mp3"); + testGenericFrameFactory( + copy.fileName().c_str(), + [](const char *fileName) { + return new MPEG::File(fileName); + }, + [](const char *fileName, ID3v2::FrameFactory *factory) { + return new MPEG::File(fileName, factory); + }, + [](const File &f) { + return static_cast(f).hasID3v2Tag(); + }, + [](File &f) { + return static_cast(f).ID3v2Tag(true); + }, + [](File &f) { + return static_cast(f).strip(); + } + ); + } + + void testFLAC() + { + ScopedFileCopy copy("no-tags", ".flac"); + testGenericFrameFactory( + copy.fileName().c_str(), + [](const char *fileName) { + return new FLAC::File(fileName); + }, + [](const char *fileName, ID3v2::FrameFactory *factory) { + return new FLAC::File(fileName, factory); + }, + [](const File &f) { + return static_cast(f).hasID3v2Tag(); + }, + [](File &f) { + return static_cast(f).ID3v2Tag(true); + }, + [](File &f) { + static_cast(f).strip(); + return f.save(); + } + ); + } + + void testTrueAudio() + { + ScopedFileCopy copy("empty", ".tta"); + testGenericFrameFactory( + copy.fileName().c_str(), + [](const char *fileName) { + return new TrueAudio::File(fileName); + }, + [](const char *fileName, ID3v2::FrameFactory *factory) { + return new TrueAudio::File(fileName, factory); + }, + [](const File &f) { + return static_cast(f).hasID3v2Tag(); + }, + [](File &f) { + return static_cast(f).ID3v2Tag(true); + }, + [](File &f) { + static_cast(f).strip(); + return f.save(); + } + ); + } + + void testWAV() + { + ScopedFileCopy copy("empty", ".wav"); + testGenericFrameFactory( + copy.fileName().c_str(), + [](const char *fileName) { + return new RIFF::WAV::File(fileName); + }, + [](const char *fileName, ID3v2::FrameFactory *factory) { + return new RIFF::WAV::File( + fileName, true, RIFF::WAV::Properties::Average, factory); + }, + [](const File &f) { + return static_cast(f).hasID3v2Tag(); + }, + [](File &f) { + return static_cast(f).tag(); + }, + [](File &f) { + static_cast(f).strip(); + return true; + } + ); + } + + void testAIFF() + { + ScopedFileCopy copy("empty", ".aiff"); + testGenericFrameFactory( + copy.fileName().c_str(), + [](const char *fileName) { + return new RIFF::AIFF::File(fileName); + }, + [](const char *fileName, ID3v2::FrameFactory *factory) { + return new RIFF::AIFF::File( + fileName, true, RIFF::AIFF::Properties::Average, factory); + }, + [](const File &f) { + return static_cast(f).hasID3v2Tag(); + }, + [](File &f) { + return static_cast(f).tag(); + }, + [](File &f) { + f.setProperties({}); + return f.save(); + } + ); + } + + void testDSF() + { + ScopedFileCopy copy("empty10ms", ".dsf"); + testGenericFrameFactory( + copy.fileName().c_str(), + [](const char *fileName) { + return new DSF::File(fileName); + }, + [](const char *fileName, ID3v2::FrameFactory *factory) { + return new DSF::File( + fileName, true, DSF::Properties::Average, factory); + }, + [](const File &f) { + return !f.tag()->isEmpty(); + }, + [](File &f) { + return static_cast(f).tag(); + }, + [](File &f) { + f.setProperties({}); + return f.save(); + } + ); + } + + void testDSDIFF() + { + ScopedFileCopy copy("empty10ms", ".dff"); + testGenericFrameFactory( + copy.fileName().c_str(), + [](const char *fileName) { + return new DSDIFF::File(fileName); + }, + [](const char *fileName, ID3v2::FrameFactory *factory) { + return new DSDIFF::File( + fileName, true, DSDIFF::Properties::Average, factory); + }, + [](const File &f) { + return static_cast(f).hasID3v2Tag(); + }, + [](File &f) { + return static_cast(f).ID3v2Tag(true); + }, + [](File &f) { + static_cast(f).strip(); + return true; + } + ); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestId3v2FrameFactory);