Fix extensibility of ID3v2 FrameFactory

Because the main extension point of FrameFactory was using a protected
Frame subclass, it was not really possible to implement a custom frame
factory. Existing Frame subclasses also show that access to the frame
header might be needed when implementing a Frame subclass.
This commit is contained in:
Urs Fleisch 2023-11-18 07:14:32 +01:00
parent 59166f6757
commit 3d67b139e4
9 changed files with 643 additions and 247 deletions

View File

@ -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
////////////////////////////////////////////////////////////////////////////////

View File

@ -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 &);

View File

@ -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)) {

View File

@ -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:

View File

@ -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);
}

View File

@ -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 <i>abstract factory</i> and <i>singleton</i> patterns
* of which more information is available on the web and in software design
* textbooks (Notably <i>Design Patters</i>).
* textbooks (Notably <i>Design Patterns</i>).
*
* \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

View File

@ -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
}

View File

@ -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

View File

@ -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 <functional>
#include <memory>
#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 <cppunit/extensions/HelperMacros.h>
#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<File *(const char *)> createFileWithDefaultFactory,
function<File *(const char *, ID3v2::FrameFactory *factory)> createFileWithFactory,
function<bool(const File &)> hasID3v2Tag,
function<ID3v2::Tag *(File &)> getID3v2Tag,
function<bool(File &)> stripAllTags)
{
CustomFrameFactory factory;
{
auto f = std::unique_ptr<File>(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<File>(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<File>(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<CustomFrame *>(frames.front()));
}
{
auto f = std::unique_ptr<File>(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<CustomFrame *>(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<File>(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<File>(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<CustomFrame *>(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<const MPEG::File &>(f).hasID3v2Tag();
},
[](File &f) {
return static_cast<MPEG::File &>(f).ID3v2Tag(true);
},
[](File &f) {
return static_cast<MPEG::File &>(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<const FLAC::File &>(f).hasID3v2Tag();
},
[](File &f) {
return static_cast<FLAC::File &>(f).ID3v2Tag(true);
},
[](File &f) {
static_cast<FLAC::File &>(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<const TrueAudio::File &>(f).hasID3v2Tag();
},
[](File &f) {
return static_cast<TrueAudio::File &>(f).ID3v2Tag(true);
},
[](File &f) {
static_cast<TrueAudio::File &>(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<const RIFF::WAV::File &>(f).hasID3v2Tag();
},
[](File &f) {
return static_cast<RIFF::WAV::File &>(f).tag();
},
[](File &f) {
static_cast<RIFF::WAV::File &>(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<const RIFF::AIFF::File &>(f).hasID3v2Tag();
},
[](File &f) {
return static_cast<RIFF::AIFF::File &>(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<DSF::File &>(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<const DSDIFF::File &>(f).hasID3v2Tag();
},
[](File &f) {
return static_cast<DSDIFF::File &>(f).ID3v2Tag(true);
},
[](File &f) {
static_cast<DSDIFF::File &>(f).strip();
return true;
}
);
}
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestId3v2FrameFactory);