{"action":"created","comment":{"url":"https://api.github.com/repos/mixxxdj/mixxx/pulls/comments/24089547","id":24089547,"diff_hunk":"@@ -0,0 +1,768 @@\n+#include \"metadata/trackmetadatataglib.h\"\n+\n+#include <taglib/tfile.h>\n+#include <taglib/tag.h>\n+#include <taglib/audioproperties.h>\n+#include <taglib/vorbisfile.h>\n+#include <taglib/id3v2frame.h>\n+#include <taglib/id3v2header.h>\n+#include <taglib/id3v1tag.h>\n+#include <taglib/tmap.h>\n+#include <taglib/tstringlist.h>\n+#include <taglib/textidentificationframe.h>\n+#include <taglib/wavpackfile.h>\n+#include <taglib/attachedpictureframe.h>\n+#include <taglib/flacpicture.h>\n+\n+#include <QStringList>\n+#include <QDebug>\n+\n+namespace Mixxx {\n+\n+// static\n+namespace {\n+const bool kDebugMetadata = false;\n+\n+// Taglib strings can be NULL and using it could cause some segfaults,\n+// so in this case it will return a QString()\n+inline QString toQString(const TagLib::String& tString) {\n+    if (tString.isNull()) {\n+        return QString();\n+    } else {\n+        return TStringToQString(tString);\n+    }\n+}\n+\n+// Returns the first element of TagLib string list.\n+inline QString toQStringFirst(const TagLib::StringList& strList) {\n+    if (strList.isEmpty()) {\n+        return QString();\n+    } else {\n+        return toQString(strList.front());\n+    }\n+}\n+\n+// Returns the text of an ID3v2 frame as a string.\n+inline QString toQString(const TagLib::ID3v2::Frame& frame) {\n+    return toQString(frame.toString());\n+}\n+\n+// Returns the first frame of an ID3v2 tag as a string.\n+inline QString toQStringFirst(const TagLib::ID3v2::FrameList& frameList) {\n+    if (frameList.isEmpty() || (NULL == frameList.front())) {\n+        return QString();\n+    } else {\n+        return toQString(*frameList.front());\n+    }\n+}\n+\n+// Returns the first value of an MP4 item as a string.\n+inline QString toQStringFirst(const TagLib::MP4::Item& mp4Item) {\n+    const TagLib::StringList strList(mp4Item.toStringList());\n+    if (strList.isEmpty()) {\n+        return QString();\n+    } else {\n+        return toQString(strList.front());\n+    }\n+}\n+\n+// Returns an APE item as a string.\n+inline QString toQString(const TagLib::APE::Item& apeItem) {\n+    return toQString(apeItem.toString());\n+}\n+\n+inline TagLib::String toTagLibString(const QString& str, TagLib::String::Type textType = TagLib::String::UTF8) {\n+    const QByteArray qba(str.toUtf8());\n+    return TagLib::String(qba.constData(), textType);\n+}\n+\n+inline bool parseBpm(TrackMetadata* pTrackMetadata, QString sBpm) {\n+    bool bpmValid = false;\n+    double bpm = TrackMetadata::parseBpm(sBpm, &bpmValid);\n+    if (bpmValid) {\n+        pTrackMetadata->setBpm(bpm);\n+    }\n+    return bpmValid;\n+}\n+\n+inline bool parseReplayGain(TrackMetadata* pTrackMetadata, QString sReplayGain) {\n+    bool replayGainValid = false;\n+    double replayGain = TrackMetadata::parseReplayGain(sReplayGain, &replayGainValid);\n+    if (replayGainValid) {\n+        pTrackMetadata->setReplayGain(replayGain);\n+    }\n+    return replayGainValid;\n+}\n+\n+} // anonymous namespace\n+\n+bool readAudioProperties(TrackMetadata* pTrackMetadata,\n+        const TagLib::File& file) {\n+    if (kDebugMetadata) {\n+        qDebug() << \"Parsing\" << file.name();\n+    }\n+\n+    if (file.isValid()) {\n+        const TagLib::AudioProperties *properties = file.audioProperties();\n+        if (properties) {\n+            pTrackMetadata->setChannels(properties->channels());\n+            pTrackMetadata->setSampleRate(properties->sampleRate());\n+            pTrackMetadata->setDuration(properties->length());\n+            pTrackMetadata->setBitrate(properties->bitrate());\n+\n+            if (kDebugMetadata) {\n+                qDebug() << \"TagLib\" << \"channels\"\n+                        << pTrackMetadata->getChannels() << \"sampleRate\"\n+                        << pTrackMetadata->getSampleRate() << \"bitrate\"\n+                        << pTrackMetadata->getBitrate() << \"duration\"\n+                        << pTrackMetadata->getDuration();\n+            }\n+\n+            return true;\n+        }\n+    }\n+    return false;\n+}\n+\n+void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag) {\n+    pTrackMetadata->setTitle(toQString(tag.title()));\n+    pTrackMetadata->setArtist(toQString(tag.artist()));\n+    pTrackMetadata->setAlbum(toQString(tag.album()));\n+    pTrackMetadata->setComment(toQString(tag.comment()));\n+    pTrackMetadata->setGenre(toQString(tag.genre()));\n+\n+    int iYear = tag.year();\n+    if (iYear > 0) {\n+        pTrackMetadata->setYear(QString::number(iYear));\n+    }\n+\n+    int iTrack = tag.track();\n+    if (iTrack > 0) {\n+        pTrackMetadata->setTrackNumber(QString::number(iTrack));\n+    }\n+\n+    if (kDebugMetadata) {\n+        qDebug() << \"TagLib\" << \"title\" << pTrackMetadata->getTitle()\n+                << \"artist\" << pTrackMetadata->getArtist() << \"album\"\n+                << pTrackMetadata->getAlbum() << \"comment\"\n+                << pTrackMetadata->getComment() << \"genre\"\n+                << pTrackMetadata->getGenre() << \"year\"\n+                << pTrackMetadata->getYear() << \"trackNumber\"\n+                << pTrackMetadata->getTrackNumber();\n+    }\n+}\n+\n+void readID3v2Tag(TrackMetadata* pTrackMetadata,\n+        const TagLib::ID3v2::Tag& tag) {\n+\n+    // Print every frame in the file.\n+    if (kDebugMetadata) {\n+        for (TagLib::ID3v2::FrameList::ConstIterator it(\n+                tag.frameList().begin()); it != tag.frameList().end(); ++it) {\n+            qDebug() << \"ID3V2\" << (*it)->frameID().data() << \"-\"\n+                    << toQString((*it)->toString());\n+        }\n+    }\n+\n+    readTag(pTrackMetadata, tag);\n+\n+    const TagLib::ID3v2::FrameList albumArtistFrame(tag.frameListMap()[\"TPE2\"]);\n+    if (!albumArtistFrame.isEmpty()) {\n+        pTrackMetadata->setAlbumArtist(toQStringFirst(albumArtistFrame));\n+    }\n+\n+    if (pTrackMetadata->getAlbum().isEmpty()) {\n+        const TagLib::ID3v2::FrameList originalAlbumFrame(\n+                tag.frameListMap()[\"TOAL\"]);\n+        pTrackMetadata->setAlbum(toQStringFirst(originalAlbumFrame));\n+    }\n+\n+    const TagLib::ID3v2::FrameList composerFrame(tag.frameListMap()[\"TCOM\"]);\n+    if (!composerFrame.isEmpty()) {\n+        pTrackMetadata->setComposer(toQStringFirst(composerFrame));\n+    }\n+\n+    const TagLib::ID3v2::FrameList groupingFrame(tag.frameListMap()[\"TIT1\"]);\n+    if (!groupingFrame.isEmpty()) {\n+        pTrackMetadata->setGrouping(toQStringFirst(groupingFrame));\n+    }\n+\n+    // ID3v2.4.0: TDRC replaces TYER + TDAT\n+    const QString recordingTime(\n+            toQStringFirst(tag.frameListMap()[\"TDRC\"]));\n+    if (!recordingTime.isEmpty()) {\n+        pTrackMetadata->setYear(recordingTime);\n+    } else {\n+        // Fallback to TYER + TDAT according to http://id3.org/id3v2.3.0\n+        // NOTE(uklotzde): We only check the length of both fields, but\n+        // not if they actually contain numeric strings.\n+        const QString recordingYear(\n+                toQStringFirst(tag.frameListMap()[\"TYER\"]).trimmed());\n+        // \"TYER: The 'Year' frame is a numeric string with a year of the\n+        // recording. This frame is always four characters long (until\n+        // the year 10000).\"\n+        QString year(recordingYear);\n+        if (4 == recordingYear.length()) {\n+            // \"TDAT:  The 'Date' frame is a numeric string in the DDMM\n+            // format containing the date for the recording. This field\n+            // is always four characters long.\n+            const QString recordingDate(\n+                    toQStringFirst(tag.frameListMap()[\"TDAT\"]).trimmed());\n+            if (4 == recordingDate.length()) {\n+                year += '-';\n+                year += recordingDate.left(2);\n+                year += '-';\n+                year += recordingDate.right(2);\n+            }\n+        }\n+        if (!year.isEmpty()) {\n+            pTrackMetadata->setYear(year);\n+        }\n+    }\n+\n+    const TagLib::ID3v2::FrameList bpmFrame(tag.frameListMap()[\"TBPM\"]);\n+    if (!bpmFrame.isEmpty()) {\n+        parseBpm(pTrackMetadata, toQStringFirst(bpmFrame));\n+    }\n+\n+    // Foobar2000-style ID3v2.3.0 tags\n+    // TODO: Check if everything is ok.\n+    TagLib::ID3v2::FrameList textFrames(tag.frameListMap()[\"TXXX\"]);\n+    for (TagLib::ID3v2::FrameList::ConstIterator it(textFrames.begin());\n+            it != textFrames.end(); ++it) {\n+        TagLib::ID3v2::UserTextIdentificationFrame* replaygainFrame =\n+                dynamic_cast<TagLib::ID3v2::UserTextIdentificationFrame*>(*it);\n+        if (replaygainFrame && replaygainFrame->fieldList().size() >= 2) {\n+            const QString desc(\n+                    toQString(replaygainFrame->description()));\n+            // Only read track gain (not album gain)\n+            if (0 == desc.compare(\"REPLAYGAIN_TRACK_GAIN\", Qt::CaseInsensitive)) {\n+                parseReplayGain(pTrackMetadata,\n+                        toQString(replaygainFrame->fieldList()[1]));\n+            }\n+        }\n+    }\n+\n+    const TagLib::ID3v2::FrameList keyFrame(tag.frameListMap()[\"TKEY\"]);\n+    if (!keyFrame.isEmpty()) {\n+        pTrackMetadata->setKey(toQStringFirst(keyFrame));\n+    }\n+}\n+\n+void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) {\n+    if (kDebugMetadata) {\n+        for (TagLib::APE::ItemListMap::ConstIterator it(\n+                tag.itemListMap().begin()); it != tag.itemListMap().end();\n+                ++it) {\n+            qDebug() << \"APE\" << toQString((*it).first) << \"-\"\n+                    << toQString((*it).second.toString());\n+        }\n+    }\n+\n+    readTag(pTrackMetadata, tag);\n+\n+    if (tag.itemListMap().contains(\"Album Artist\")) {\n+        pTrackMetadata->setAlbumArtist(\n+                toQString(tag.itemListMap()[\"Album Artist\"]));\n+    }\n+\n+    if (tag.itemListMap().contains(\"Composer\")) {\n+        pTrackMetadata->setComposer(toQString(tag.itemListMap()[\"Composer\"]));\n+    }\n+\n+    if (tag.itemListMap().contains(\"Grouping\")) {\n+        pTrackMetadata->setGrouping(toQString(tag.itemListMap()[\"Grouping\"]));\n+    }\n+\n+    // The release date (ISO 8601 without 'T' separator between date and time)\n+    // according to the mapping used by MusicBrainz Picard.\n+    // http://wiki.hydrogenaud.io/index.php?title=APE_date\n+    // https://picard.musicbrainz.org/docs/mappings\n+    if (tag.itemListMap().contains(\"Year\")) {\n+        pTrackMetadata->setYear(toQString(tag.itemListMap()[\"Year\"]));\n+    }\n+\n+    if (tag.itemListMap().contains(\"BPM\")) {\n+        parseBpm(pTrackMetadata, toQString(tag.itemListMap()[\"BPM\"]));\n+    }\n+\n+    // Only read track gain (not album gain)\n+    if (tag.itemListMap().contains(\"REPLAYGAIN_TRACK_GAIN\")) {\n+        parseReplayGain(pTrackMetadata,\n+                toQString(tag.itemListMap()[\"REPLAYGAIN_TRACK_GAIN\"]));\n+    }\n+}\n+\n+void readXiphComment(TrackMetadata* pTrackMetadata,\n+        const TagLib::Ogg::XiphComment& tag) {\n+    if (kDebugMetadata) {\n+        for (TagLib::Ogg::FieldListMap::ConstIterator it(\n+                tag.fieldListMap().begin()); it != tag.fieldListMap().end();\n+                ++it) {\n+            qDebug() << \"XIPH\" << toQString((*it).first) << \"-\"\n+                    << toQString((*it).second.toString());\n+        }\n+    }\n+\n+    readTag(pTrackMetadata, tag);\n+\n+    // Some applications (like puddletag up to version 1.0.5) write COMMENT\n+    // instead DESCRIPTION. If the comment field (correctly populated by TagLib\n+    // from DESCRIPTION) is still empty we will additionally read this field.\n+    // Reference: http://www.xiph.org/vorbis/doc/v-comment.html\n+    if (pTrackMetadata->getComment().isEmpty()\n+            && tag.fieldListMap().contains(\"COMMENT\")) {\n+        pTrackMetadata->setComment(\n+                toQStringFirst(tag.fieldListMap()[\"COMMENT\"]));\n+    }\n+\n+    if (tag.fieldListMap().contains(\"ALBUMARTIST\")) {\n+        pTrackMetadata->setAlbumArtist(\n+                toQStringFirst(tag.fieldListMap()[\"ALBUMARTIST\"]));\n+    }\n+    if (pTrackMetadata->getAlbumArtist().isEmpty()\n+            && tag.fieldListMap().contains(\"ALBUM_ARTIST\")) {\n+        // try alternative field name\n+        pTrackMetadata->setAlbumArtist(\n+                toQStringFirst(tag.fieldListMap()[\"ALBUM_ARTIST\"]));\n+    }\n+    if (pTrackMetadata->getAlbumArtist().isEmpty()\n+            && tag.fieldListMap().contains(\"ALBUM ARTIST\")) {\n+        // try alternative field name\n+        pTrackMetadata->setAlbumArtist(\n+                toQStringFirst(tag.fieldListMap()[\"ALBUM ARTIST\"]));\n+    }\n+\n+    if (tag.fieldListMap().contains(\"COMPOSER\")) {\n+        pTrackMetadata->setComposer(\n+                toQStringFirst(tag.fieldListMap()[\"COMPOSER\"]));\n+    }\n+\n+    if (tag.fieldListMap().contains(\"GROUPING\")) {\n+        pTrackMetadata->setGrouping(\n+                toQStringFirst(tag.fieldListMap()[\"GROUPING\"]));\n+    }\n+\n+    // The release date formatted according to ISO 8601. Might\n+    // be followed by a space character and arbitrary text.\n+    // http://age.hobba.nl/audio/mirroredpages/ogg-tagging.html\n+    if (tag.fieldListMap().contains(\"DATE\")) {\n+        pTrackMetadata->setYear(toQStringFirst(tag.fieldListMap()[\"DATE\"]));\n+    }\n+\n+    // Some tags use \"BPM\" so check for that.\n+    if (tag.fieldListMap().contains(\"BPM\")) {\n+        parseBpm(pTrackMetadata, toQStringFirst(tag.fieldListMap()[\"BPM\"]));\n+    }\n+\n+    // Give preference to the \"TEMPO\" tag which seems to be more standard\n+    if (tag.fieldListMap().contains(\"TEMPO\")) {\n+        parseBpm(pTrackMetadata, toQStringFirst(tag.fieldListMap()[\"TEMPO\"]));\n+    }\n+\n+    // Only read track gain (not album gain)\n+    if (tag.fieldListMap().contains(\"REPLAYGAIN_TRACK_GAIN\")) {\n+        parseReplayGain(pTrackMetadata,\n+                toQStringFirst(tag.fieldListMap()[\"REPLAYGAIN_TRACK_GAIN\"]));\n+    }\n+\n+    /*\n+     * Reading key code information\n+     * Unlike, ID3 tags, there's no standard or recommendation on how to store 'key' code\n+     *\n+     * Luckily, there are only a few tools for that, e.g., Rapid Evolution (RE).\n+     * Assuming no distinction between start and end key, RE uses a \"INITIALKEY\"\n+     * or a \"KEY\" vorbis comment.\n+     */\n+    if (tag.fieldListMap().contains(\"KEY\")) {\n+        pTrackMetadata->setKey(toQStringFirst(tag.fieldListMap()[\"KEY\"]));\n+    }\n+    if (tag.fieldListMap().contains(\"INITIALKEY\")) {\n+        // This is the preferred field for storing the musical key.\n+        pTrackMetadata->setKey(\n+                toQStringFirst(tag.fieldListMap()[\"INITIALKEY\"]));\n+    }\n+}\n+\n+void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) {\n+    if (kDebugMetadata) {\n+        for (TagLib::MP4::ItemListMap::ConstIterator it(\n+                tag.itemListMap().begin()); it != tag.itemListMap().end();\n+                ++it) {\n+            qDebug() << \"MP4\" << toQString((*it).first) << \"-\"\n+                    << toQStringFirst((*it).second);\n+        }\n+    }\n+\n+    readTag(pTrackMetadata, tag);\n+\n+    // Get Album Artist\n+    if (tag.itemListMap().contains(\"aART\")) {\n+        pTrackMetadata->setAlbumArtist(\n+                toQStringFirst(tag.itemListMap()[\"aART\"]));\n+    }\n+\n+    // Get Composer\n+    if (tag.itemListMap().contains(\"\\251wrt\")) {\n+        pTrackMetadata->setComposer(\n+                toQStringFirst(tag.itemListMap()[\"\\251wrt\"]));\n+    }\n+\n+    // Get Grouping\n+    if (tag.itemListMap().contains(\"\\251grp\")) {\n+        pTrackMetadata->setGrouping(\n+                toQStringFirst(tag.itemListMap()[\"\\251grp\"]));\n+    }\n+\n+    // Get date/year as string\n+    if (tag.itemListMap().contains(\"\\251day\")) {\n+        pTrackMetadata->setYear(toQStringFirst(tag.itemListMap()[\"\\251day\"]));\n+    }\n+\n+    // Get BPM\n+    if (tag.itemListMap().contains(\"tmpo\")) {\n+        // Read the BPM as an integer value.\n+        const TagLib::MP4::Item& item = tag.itemListMap()[\"tmpo\"];\n+        if (item.atomDataType() == TagLib::MP4::TypeInteger) {\n+            pTrackMetadata->setBpm(tag.itemListMap()[\"tmpo\"].toInt());\n+        }\n+    }\n+    if (tag.itemListMap().contains(\"----:com.apple.iTunes:BPM\")) {\n+        // This is the preferred field for storing the BPM\n+        // with fractional digits as a floating-point value.\n+        // If this field contains a valid value the integer\n+        // BPM value that might have been read before is\n+        // overwritten.\n+        parseBpm(pTrackMetadata,\n+                toQStringFirst(tag.itemListMap()[\"----:com.apple.iTunes:BPM\"]));\n+    }\n+\n+    // Only read track gain (not album gain)\n+    if (tag.itemListMap().contains(\n+            \"----:com.apple.iTunes:replaygain_track_gain\")) {\n+        parseReplayGain(pTrackMetadata,\n+                toQStringFirst(tag.itemListMap()[\"----:com.apple.iTunes:replaygain_track_gain\"]));\n+    }\n+\n+    // Read musical key (conforms to Rapid Evolution)\n+    if (tag.itemListMap().contains(\"----:com.apple.iTunes:KEY\")) {\n+        pTrackMetadata->setKey(\n+                toQStringFirst(tag.itemListMap()[\"----:com.apple.iTunes:KEY\"]));\n+    }\n+    // Read musical key (conforms to MixedInKey, Serato, Traktor)\n+    if (tag.itemListMap().contains(\"----:com.apple.iTunes:initialkey\")) {\n+        // This is the preferred field for storing the musical key!\n+        pTrackMetadata->setKey(\n+                toQStringFirst(tag.itemListMap()[\"----:com.apple.iTunes:initialkey\"]));\n+    }\n+}\n+\n+QImage readID3v2TagCover(const TagLib::ID3v2::Tag& tag) {\n+    QImage coverArt;\n+    TagLib::ID3v2::FrameList covertArtFrame = tag.frameListMap()[\"APIC\"];\n+    if (!covertArtFrame.isEmpty()) {\n+        TagLib::ID3v2::AttachedPictureFrame* picframe =\n+                static_cast<TagLib::ID3v2::AttachedPictureFrame*>(covertArtFrame.front());\n+        TagLib::ByteVector data = picframe->picture();\n+        coverArt = QImage::fromData(\n+                reinterpret_cast<const uchar *>(data.data()), data.size());\n+    }\n+    return coverArt;\n+}\n+\n+QImage readAPETagCover(const TagLib::APE::Tag& tag) {\n+    QImage coverArt;\n+    if (tag.itemListMap().contains(\"COVER ART (FRONT)\")) {\n+        const TagLib::ByteVector nullStringTerminator(1, 0);\n+        TagLib::ByteVector item =\n+                tag.itemListMap()[\"COVER ART (FRONT)\"].value();\n+        int pos = item.find(nullStringTerminator);  // skip the filename\n+        if (++pos > 0) {\n+            const TagLib::ByteVector& data = item.mid(pos);\n+            coverArt = QImage::fromData(\n+                    reinterpret_cast<const uchar *>(data.data()), data.size());\n+        }\n+    }\n+    return coverArt;\n+}\n+\n+QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& tag) {\n+    QImage coverArt;\n+    if (tag.fieldListMap().contains(\"METADATA_BLOCK_PICTURE\")) {\n+        QByteArray data(\n+                QByteArray::fromBase64(\n+                        tag.fieldListMap()[\"METADATA_BLOCK_PICTURE\"].front().toCString()));\n+        TagLib::ByteVector tdata(data.data(), data.size());\n+        TagLib::FLAC::Picture p(tdata);\n+        data = QByteArray(p.data().data(), p.data().size());\n+        coverArt = QImage::fromData(data);\n+    } else if (tag.fieldListMap().contains(\"COVERART\")) {\n+        QByteArray data(\n+                QByteArray::fromBase64(\n+                        tag.fieldListMap()[\"COVERART\"].toString().toCString()));\n+        coverArt = QImage::fromData(data);\n+    }\n+    return coverArt;\n+}\n+\n+QImage readMP4TagCover(/*const*/TagLib::MP4::Tag& tag) {\n+    QImage coverArt;\n+    if (tag.itemListMap().contains(\"covr\")) {\n+        TagLib::MP4::CoverArtList coverArtList =\n+                tag.itemListMap()[\"covr\"].toCoverArtList();\n+        TagLib::ByteVector data = coverArtList.front().data();\n+        coverArt = QImage::fromData(\n+                reinterpret_cast<const uchar *>(data.data()), data.size());\n+    }\n+    return coverArt;\n+}\n+\n+namespace {\n+\n+void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) {\n+    pTag->removeFrames(pFrame->frameID());\n+    pTag->addFrame(pFrame);\n+}\n+\n+void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag,\n+        const TagLib::ByteVector &id, const QString& text) {\n+    TagLib::String::Type textType;\n+    // For an overview of the character encodings supported by\n+    // the different ID3v2 versions please refer to the following\n+    // resources:\n+    // http://en.wikipedia.org/wiki/ID3#ID3v2\n+    // http://id3.org/id3v2.3.0\n+    // http://id3.org/id3v2.4.0-structure\n+    if (4 <= pTag->header()->majorVersion()) {\n+        // For ID3v2.4.0 or higher prefer UTF-8, because it is a\n+        // very compact representation for common cases and it is\n+        // independent of the byte order.\n+        textType = TagLib::String::UTF8;\n+    } else {\n+        // For ID3v2.3.0/ID3v2.2.0 use UCS-2 (UTF-16 encoded Unicode\n+        // with BOM), because UTF-8 and UTF-16BE are only supported\n+        // since ID3v2.4.0 and the alternative ISO-8859-1 does not\n+        // cover all Unicode characters.\n+        textType = TagLib::String::UTF16;\n+    }\n+    QScopedPointer<TagLib::ID3v2::TextIdentificationFrame> pNewFrame(\n+            new TagLib::ID3v2::TextIdentificationFrame(id, textType));\n+    pNewFrame->setText(toTagLibString(text));\n+    replaceID3v2Frame(pTag, pNewFrame.data());\n+    // Now the plain pointer in pNewFrame is owned and\n+    // managed by pTag. We need to release the ownership\n+    // to avoid double deletion!\n+    pNewFrame.take();\n+}\n+\n+} // anonymous namespace\n+\n+bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) {\n+    if (NULL == pTag) {\n+        return false;\n+    }\n+\n+    pTag->setArtist(toTagLibString(trackMetadata.getArtist()));\n+    pTag->setTitle(toTagLibString(trackMetadata.getTitle()));\n+    pTag->setAlbum(toTagLibString(trackMetadata.getAlbum()));\n+    pTag->setGenre(toTagLibString(trackMetadata.getGenre()));\n+    pTag->setComment(toTagLibString(trackMetadata.getComment()));\n+\n+    // Only write the year into the common tag if the corresponding\n+    // field contains a 4-character numeric string.\n+    const QString yearString(trackMetadata.getYear().trimmed());\n+    if (4 == yearString.length()) {\n+        bool yearValid = false;\n+        uint year = trackMetadata.getYear().toUInt(&yearValid);\n+        if (yearValid && (year > 0)) {\n+            pTag->setYear(year);\n+        }\n+    }\n+    // Derived tags might be able to write the complete string\n+    // from trackMetadata.getYear() into the corresponding field.\n+\n+    bool trackNumberValid = false;\n+    uint track = trackMetadata.getTrackNumber().toUInt(&trackNumberValid);\n+    if (trackNumberValid && (track > 0)) {\n+        pTag->setTrack(track);\n+    }\n+\n+    return true;\n+}\n+\n+bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag,\n+        const TrackMetadata& trackMetadata) {\n+    if (NULL == pTag) {\n+        return false;\n+    }\n+    const TagLib::ID3v2::Header* pHeader = pTag->header();\n+    if ((NULL == pHeader) || (3 > pHeader->majorVersion())) {\n+        // only ID3v2.3.x and higher (currently only ID3v2.4.x) are supported\n+        return false;\n+    }\n+\n+    // Write common metadata\n+    writeTag(pTag, trackMetadata);\n+\n+    // additional tags\n+    writeID3v2TextIdentificationFrame(pTag, \"TPE2\",\n+            trackMetadata.getAlbumArtist());\n+    // According to the specification \"The 'TBPM' frame contains the number\n+    // of beats per minute in the mainpart of the audio. The BPM is an integer\n+    // and represented as a numerical string.\"\n+    // Reference: http://id3.org/id3v2.3.0\n+    writeID3v2TextIdentificationFrame(pTag, \"TBPM\",\n+            TrackMetadata::formatBpm(trackMetadata.getBpmAsInteger()));\n+    writeID3v2TextIdentificationFrame(pTag, \"TKEY\", trackMetadata.getKey());\n+    writeID3v2TextIdentificationFrame(pTag, \"TCOM\",\n+            trackMetadata.getComposer());\n+    writeID3v2TextIdentificationFrame(pTag, \"TIT1\",\n+            trackMetadata.getGrouping());\n+    if (4 <= pHeader->majorVersion()) {\n+        // ID3v2.4.0: TDRC replaces TYER + TDAT\n+        writeID3v2TextIdentificationFrame(pTag, \"TDRC\",\n+                trackMetadata.getYear());\n+    } else {\n+        // Fallback: Write TYER and TDAT\n+        const QStringList yearParts(trackMetadata.getYear().split('-'));\n+        if (0 < yearParts.length()) {\n+            const QString year(yearParts[0].trimmed());\n+            if (4 == year.length()) {\n+                writeID3v2TextIdentificationFrame(pTag, \"TYER\",\n+                        year); // yyyy\n+                if (3 == yearParts.length()) {\n+                    const QString month(yearParts[1].trimmed());\n+                    const QString day(yearParts[2].trimmed());\n+                    if ((2 == month.length()) && (2 == day.length())) {","path":"src/metadata/trackmetadatataglib.cpp","position":636,"original_position":636,"commit_id":"0f15cc46afac82d373b463ed42cc4829473baed5","original_commit_id":"0f15cc46afac82d373b463ed42cc4829473baed5","user":{"login":"daschuer","id":1777442,"avatar_url":"https://avatars.githubusercontent.com/u/1777442?v=3","gravatar_id":"","url":"https://api.github.com/users/daschuer","html_url":"https://github.com/daschuer","followers_url":"https://api.github.com/users/daschuer/followers","following_url":"https://api.github.com/users/daschuer/following{/other_user}","gists_url":"https://api.github.com/users/daschuer/gists{/gist_id}","starred_url":"https://api.github.com/users/daschuer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/daschuer/subscriptions","organizations_url":"https://api.github.com/users/daschuer/orgs","repos_url":"https://api.github.com/users/daschuer/repos","events_url":"https://api.github.com/users/daschuer/events{/privacy}","received_events_url":"https://api.github.com/users/daschuer/received_events","type":"User","site_admin":false},"body":"2 == day.length() \r\nwill fail in the unlikely case that we have Time info, separated by \" \" or \"T\" \r\n","created_at":"2015-02-04T15:15:06Z","updated_at":"2015-02-04T15:15:06Z","html_url":"https://github.com/mixxxdj/mixxx/pull/411#discussion_r24089547","pull_request_url":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411","_links":{"self":{"href":"https://api.github.com/repos/mixxxdj/mixxx/pulls/comments/24089547"},"html":{"href":"https://github.com/mixxxdj/mixxx/pull/411#discussion_r24089547"},"pull_request":{"href":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411"}}},"pull_request":{"url":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411","id":25228389,"html_url":"https://github.com/mixxxdj/mixxx/pull/411","diff_url":"https://github.com/mixxxdj/mixxx/pull/411.diff","patch_url":"https://github.com/mixxxdj/mixxx/pull/411.patch","issue_url":"https://api.github.com/repos/mixxxdj/mixxx/issues/411","number":411,"state":"open","locked":false,"title":"New SoundSource/AudioSource API","user":{"login":"uklotzde","id":1474154,"avatar_url":"https://avatars.githubusercontent.com/u/1474154?v=3","gravatar_id":"","url":"https://api.github.com/users/uklotzde","html_url":"https://github.com/uklotzde","followers_url":"https://api.github.com/users/uklotzde/followers","following_url":"https://api.github.com/users/uklotzde/following{/other_user}","gists_url":"https://api.github.com/users/uklotzde/gists{/gist_id}","starred_url":"https://api.github.com/users/uklotzde/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/uklotzde/subscriptions","organizations_url":"https://api.github.com/users/uklotzde/orgs","repos_url":"https://api.github.com/users/uklotzde/repos","events_url":"https://api.github.com/users/uklotzde/events{/privacy}","received_events_url":"https://api.github.com/users/uklotzde/received_events","type":"User","site_admin":false},"body":"This is the first step towards a better/cleaner SoundSource API.\r\nThe operations for reading audio data have been moved into the\r\nnew abstract base class AudioSource.\r\n\r\nCurrently SoundSource inherits from AudioSource for technical reasons.\r\nThis is too much much coupling that needs to be broken up. I have\r\nalready some ideas on how to resolve it, but first this work needs to\r\nbe integrated.\r\n\r\nDuring the refactoring I've noticed, that the quality and maturity of the\r\naudio decoders for different file types greatly varies:\r\n\r\nGood/mature:\r\n- FLAC\r\n- Ogg\r\n- Opus\r\n- WavPack\r\n- libsndfile\r\n\r\nMediocre:\r\n- MP3\r\n\r\nUgly/inefficient:\r\n- M4A (2014-12-04: Code has been rewritten without legacy code borrowed from other projects)\r\n- ModPlug\r\n\r\nUntested AudioSource API:\r\n- Apple CoreAudio\r\n- Windows Media Foundation\r\n\r\nUnfinished AudioSource API (just a hack):\r\n- FFmpeg\r\n\r\nEspecially the code for the M4A decoder should be revised and cleaned up as\r\nsoon as possible, followed by a review of the MP3 decoder. Reading audio\r\ndata must work as stable and efficient as possible. This has priority over\r\nthe improvement of the internal software architecture!","created_at":"2014-11-29T23:03:10Z","updated_at":"2015-02-04T15:15:06Z","closed_at":null,"merged_at":null,"merge_commit_sha":"b31e49d9381d388f17e2443bee37694c72b6f63b","assignee":null,"milestone":{"url":"https://api.github.com/repos/mixxxdj/mixxx/milestones/2","labels_url":"https://api.github.com/repos/mixxxdj/mixxx/milestones/2/labels","id":754553,"number":2,"title":"1.13","description":"","creator":{"login":"ywwg","id":888940,"avatar_url":"https://avatars.githubusercontent.com/u/888940?v=3","gravatar_id":"","url":"https://api.github.com/users/ywwg","html_url":"https://github.com/ywwg","followers_url":"https://api.github.com/users/ywwg/followers","following_url":"https://api.github.com/users/ywwg/following{/other_user}","gists_url":"https://api.github.com/users/ywwg/gists{/gist_id}","starred_url":"https://api.github.com/users/ywwg/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ywwg/subscriptions","organizations_url":"https://api.github.com/users/ywwg/orgs","repos_url":"https://api.github.com/users/ywwg/repos","events_url":"https://api.github.com/users/ywwg/events{/privacy}","received_events_url":"https://api.github.com/users/ywwg/received_events","type":"User","site_admin":false},"open_issues":9,"closed_issues":4,"state":"open","created_at":"2014-08-14T23:06:10Z","updated_at":"2015-02-01T11:07:41Z","due_on":null,"closed_at":null},"commits_url":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411/commits","review_comments_url":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411/comments","review_comment_url":"https://api.github.com/repos/mixxxdj/mixxx/pulls/comments/{number}","comments_url":"https://api.github.com/repos/mixxxdj/mixxx/issues/411/comments","statuses_url":"https://api.github.com/repos/mixxxdj/mixxx/statuses/0f15cc46afac82d373b463ed42cc4829473baed5","head":{"label":"uklotzde:NewSoundSourceAPI","ref":"NewSoundSourceAPI","sha":"0f15cc46afac82d373b463ed42cc4829473baed5","user":{"login":"uklotzde","id":1474154,"avatar_url":"https://avatars.githubusercontent.com/u/1474154?v=3","gravatar_id":"","url":"https://api.github.com/users/uklotzde","html_url":"https://github.com/uklotzde","followers_url":"https://api.github.com/users/uklotzde/followers","following_url":"https://api.github.com/users/uklotzde/following{/other_user}","gists_url":"https://api.github.com/users/uklotzde/gists{/gist_id}","starred_url":"https://api.github.com/users/uklotzde/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/uklotzde/subscriptions","organizations_url":"https://api.github.com/users/uklotzde/orgs","repos_url":"https://api.github.com/users/uklotzde/repos","events_url":"https://api.github.com/users/uklotzde/events{/privacy}","received_events_url":"https://api.github.com/users/uklotzde/received_events","type":"User","site_admin":false},"repo":{"id":11108787,"name":"mixxx","full_name":"uklotzde/mixxx","owner":{"login":"uklotzde","id":1474154,"avatar_url":"https://avatars.githubusercontent.com/u/1474154?v=3","gravatar_id":"","url":"https://api.github.com/users/uklotzde","html_url":"https://github.com/uklotzde","followers_url":"https://api.github.com/users/uklotzde/followers","following_url":"https://api.github.com/users/uklotzde/following{/other_user}","gists_url":"https://api.github.com/users/uklotzde/gists{/gist_id}","starred_url":"https://api.github.com/users/uklotzde/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/uklotzde/subscriptions","organizations_url":"https://api.github.com/users/uklotzde/orgs","repos_url":"https://api.github.com/users/uklotzde/repos","events_url":"https://api.github.com/users/uklotzde/events{/privacy}","received_events_url":"https://api.github.com/users/uklotzde/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/uklotzde/mixxx","description":"Mixxx is Free DJ software that gives you everything you need to perform live mixes.","fork":true,"url":"https://api.github.com/repos/uklotzde/mixxx","forks_url":"https://api.github.com/repos/uklotzde/mixxx/forks","keys_url":"https://api.github.com/repos/uklotzde/mixxx/keys{/key_id}","collaborators_url":"https://api.github.com/repos/uklotzde/mixxx/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/uklotzde/mixxx/teams","hooks_url":"https://api.github.com/repos/uklotzde/mixxx/hooks","issue_events_url":"https://api.github.com/repos/uklotzde/mixxx/issues/events{/number}","events_url":"https://api.github.com/repos/uklotzde/mixxx/events","assignees_url":"https://api.github.com/repos/uklotzde/mixxx/assignees{/user}","branches_url":"https://api.github.com/repos/uklotzde/mixxx/branches{/branch}","tags_url":"https://api.github.com/repos/uklotzde/mixxx/tags","blobs_url":"https://api.github.com/repos/uklotzde/mixxx/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/uklotzde/mixxx/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/uklotzde/mixxx/git/refs{/sha}","trees_url":"https://api.github.com/repos/uklotzde/mixxx/git/trees{/sha}","statuses_url":"https://api.github.com/repos/uklotzde/mixxx/statuses/{sha}","languages_url":"https://api.github.com/repos/uklotzde/mixxx/languages","stargazers_url":"https://api.github.com/repos/uklotzde/mixxx/stargazers","contributors_url":"https://api.github.com/repos/uklotzde/mixxx/contributors","subscribers_url":"https://api.github.com/repos/uklotzde/mixxx/subscribers","subscription_url":"https://api.github.com/repos/uklotzde/mixxx/subscription","commits_url":"https://api.github.com/repos/uklotzde/mixxx/commits{/sha}","git_commits_url":"https://api.github.com/repos/uklotzde/mixxx/git/commits{/sha}","comments_url":"https://api.github.com/repos/uklotzde/mixxx/comments{/number}","issue_comment_url":"https://api.github.com/repos/uklotzde/mixxx/issues/comments/{number}","contents_url":"https://api.github.com/repos/uklotzde/mixxx/contents/{+path}","compare_url":"https://api.github.com/repos/uklotzde/mixxx/compare/{base}...{head}","merges_url":"https://api.github.com/repos/uklotzde/mixxx/merges","archive_url":"https://api.github.com/repos/uklotzde/mixxx/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/uklotzde/mixxx/downloads","issues_url":"https://api.github.com/repos/uklotzde/mixxx/issues{/number}","pulls_url":"https://api.github.com/repos/uklotzde/mixxx/pulls{/number}","milestones_url":"https://api.github.com/repos/uklotzde/mixxx/milestones{/number}","notifications_url":"https://api.github.com/repos/uklotzde/mixxx/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/uklotzde/mixxx/labels{/name}","releases_url":"https://api.github.com/repos/uklotzde/mixxx/releases{/id}","created_at":"2013-07-01T22:22:18Z","updated_at":"2015-01-12T23:41:03Z","pushed_at":"2015-02-04T07:17:36Z","git_url":"git://github.com/uklotzde/mixxx.git","ssh_url":"git@github.com:uklotzde/mixxx.git","clone_url":"https://github.com/uklotzde/mixxx.git","svn_url":"https://github.com/uklotzde/mixxx","homepage":"http://mixxx.org","size":237010,"stargazers_count":0,"watchers_count":0,"language":"C++","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"mixxxdj:master","ref":"master","sha":"3907041ce811ef0d9bfc2f41f00997f8e7f2c9ac","user":{"login":"mixxxdj","id":1743769,"avatar_url":"https://avatars.githubusercontent.com/u/1743769?v=3","gravatar_id":"","url":"https://api.github.com/users/mixxxdj","html_url":"https://github.com/mixxxdj","followers_url":"https://api.github.com/users/mixxxdj/followers","following_url":"https://api.github.com/users/mixxxdj/following{/other_user}","gists_url":"https://api.github.com/users/mixxxdj/gists{/gist_id}","starred_url":"https://api.github.com/users/mixxxdj/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mixxxdj/subscriptions","organizations_url":"https://api.github.com/users/mixxxdj/orgs","repos_url":"https://api.github.com/users/mixxxdj/repos","events_url":"https://api.github.com/users/mixxxdj/events{/privacy}","received_events_url":"https://api.github.com/users/mixxxdj/received_events","type":"Organization","site_admin":false},"repo":{"id":10126031,"name":"mixxx","full_name":"mixxxdj/mixxx","owner":{"login":"mixxxdj","id":1743769,"avatar_url":"https://avatars.githubusercontent.com/u/1743769?v=3","gravatar_id":"","url":"https://api.github.com/users/mixxxdj","html_url":"https://github.com/mixxxdj","followers_url":"https://api.github.com/users/mixxxdj/followers","following_url":"https://api.github.com/users/mixxxdj/following{/other_user}","gists_url":"https://api.github.com/users/mixxxdj/gists{/gist_id}","starred_url":"https://api.github.com/users/mixxxdj/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mixxxdj/subscriptions","organizations_url":"https://api.github.com/users/mixxxdj/orgs","repos_url":"https://api.github.com/users/mixxxdj/repos","events_url":"https://api.github.com/users/mixxxdj/events{/privacy}","received_events_url":"https://api.github.com/users/mixxxdj/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/mixxxdj/mixxx","description":"Mixxx is Free DJ software that gives you everything you need to perform live mixes.","fork":false,"url":"https://api.github.com/repos/mixxxdj/mixxx","forks_url":"https://api.github.com/repos/mixxxdj/mixxx/forks","keys_url":"https://api.github.com/repos/mixxxdj/mixxx/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mixxxdj/mixxx/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mixxxdj/mixxx/teams","hooks_url":"https://api.github.com/repos/mixxxdj/mixxx/hooks","issue_events_url":"https://api.github.com/repos/mixxxdj/mixxx/issues/events{/number}","events_url":"https://api.github.com/repos/mixxxdj/mixxx/events","assignees_url":"https://api.github.com/repos/mixxxdj/mixxx/assignees{/user}","branches_url":"https://api.github.com/repos/mixxxdj/mixxx/branches{/branch}","tags_url":"https://api.github.com/repos/mixxxdj/mixxx/tags","blobs_url":"https://api.github.com/repos/mixxxdj/mixxx/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mixxxdj/mixxx/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mixxxdj/mixxx/git/refs{/sha}","trees_url":"https://api.github.com/repos/mixxxdj/mixxx/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mixxxdj/mixxx/statuses/{sha}","languages_url":"https://api.github.com/repos/mixxxdj/mixxx/languages","stargazers_url":"https://api.github.com/repos/mixxxdj/mixxx/stargazers","contributors_url":"https://api.github.com/repos/mixxxdj/mixxx/contributors","subscribers_url":"https://api.github.com/repos/mixxxdj/mixxx/subscribers","subscription_url":"https://api.github.com/repos/mixxxdj/mixxx/subscription","commits_url":"https://api.github.com/repos/mixxxdj/mixxx/commits{/sha}","git_commits_url":"https://api.github.com/repos/mixxxdj/mixxx/git/commits{/sha}","comments_url":"https://api.github.com/repos/mixxxdj/mixxx/comments{/number}","issue_comment_url":"https://api.github.com/repos/mixxxdj/mixxx/issues/comments/{number}","contents_url":"https://api.github.com/repos/mixxxdj/mixxx/contents/{+path}","compare_url":"https://api.github.com/repos/mixxxdj/mixxx/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mixxxdj/mixxx/merges","archive_url":"https://api.github.com/repos/mixxxdj/mixxx/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mixxxdj/mixxx/downloads","issues_url":"https://api.github.com/repos/mixxxdj/mixxx/issues{/number}","pulls_url":"https://api.github.com/repos/mixxxdj/mixxx/pulls{/number}","milestones_url":"https://api.github.com/repos/mixxxdj/mixxx/milestones{/number}","notifications_url":"https://api.github.com/repos/mixxxdj/mixxx/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mixxxdj/mixxx/labels{/name}","releases_url":"https://api.github.com/repos/mixxxdj/mixxx/releases{/id}","created_at":"2013-05-17T15:14:38Z","updated_at":"2015-02-03T20:46:49Z","pushed_at":"2015-02-03T20:46:49Z","git_url":"git://github.com/mixxxdj/mixxx.git","ssh_url":"git@github.com:mixxxdj/mixxx.git","clone_url":"https://github.com/mixxxdj/mixxx.git","svn_url":"https://github.com/mixxxdj/mixxx","homepage":"http://mixxx.org","size":310967,"stargazers_count":229,"watchers_count":229,"language":"C++","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":190,"mirror_url":null,"open_issues_count":35,"forks":190,"open_issues":35,"watchers":229,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411"},"html":{"href":"https://github.com/mixxxdj/mixxx/pull/411"},"issue":{"href":"https://api.github.com/repos/mixxxdj/mixxx/issues/411"},"comments":{"href":"https://api.github.com/repos/mixxxdj/mixxx/issues/411/comments"},"review_comments":{"href":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411/comments"},"review_comment":{"href":"https://api.github.com/repos/mixxxdj/mixxx/pulls/comments/{number}"},"commits":{"href":"https://api.github.com/repos/mixxxdj/mixxx/pulls/411/commits"},"statuses":{"href":"https://api.github.com/repos/mixxxdj/mixxx/statuses/0f15cc46afac82d373b463ed42cc4829473baed5"}}}}