diff options
author | Uwe Klotz <uwe_klotz@web.de> | 2015-12-27 23:27:43 +0100 |
---|---|---|
committer | Uwe Klotz <uwe_klotz@web.de> | 2015-12-30 23:32:16 +0100 |
commit | bf14e1d6c090f0f45d829b48f9b5e947e82e6cd4 (patch) | |
tree | 0f30d53de07e39eb4546e96edacad30c12769a00 /src/track | |
parent | e2ae5ca18d3ad7e477dc9644b5ff1355c747a065 (diff) |
Merge metadata/ into track/
Diffstat (limited to 'src/track')
-rw-r--r-- | src/track/audiotagger.cpp | 28 | ||||
-rw-r--r-- | src/track/audiotagger.h | 22 | ||||
-rw-r--r-- | src/track/trackmetadata.cpp | 173 | ||||
-rw-r--r-- | src/track/trackmetadata.h | 222 | ||||
-rw-r--r-- | src/track/trackmetadatataglib.cpp | 1364 | ||||
-rw-r--r-- | src/track/trackmetadatataglib.h | 38 |
6 files changed, 1847 insertions, 0 deletions
diff --git a/src/track/audiotagger.cpp b/src/track/audiotagger.cpp new file mode 100644 index 0000000000..fa421072d9 --- /dev/null +++ b/src/track/audiotagger.cpp @@ -0,0 +1,28 @@ +#include "track/audiotagger.h" + +#include "track/trackmetadatataglib.h" + +namespace { + +inline SecurityTokenPointer openSecurityToken(QFileInfo file, + const SecurityTokenPointer& pToken) { + if (pToken.isNull()) { + return Sandbox::openSecurityToken(file, true); + } else { + return pToken; + } +} + +} // anonymous namespace + +AudioTagger::AudioTagger(const QString& file, SecurityTokenPointer pToken) : + m_file(file), + m_pSecurityToken(openSecurityToken(m_file, pToken)) { +} + +AudioTagger::~AudioTagger() { +} + +Result AudioTagger::save(const Mixxx::TrackMetadata& trackMetadata) { + return writeTrackMetadataIntoFile(trackMetadata, m_file.canonicalFilePath()); +} diff --git a/src/track/audiotagger.h b/src/track/audiotagger.h new file mode 100644 index 0000000000..1484d1e373 --- /dev/null +++ b/src/track/audiotagger.h @@ -0,0 +1,22 @@ +#ifndef AUDIOTAGGER_H +#define AUDIOTAGGER_H + +#include <QFileInfo> + +#include "track/trackmetadata.h" +#include "util/result.h" +#include "util/sandbox.h" + +class AudioTagger { +public: + AudioTagger(const QString& file, SecurityTokenPointer pToken); + virtual ~AudioTagger(); + + Result save(const Mixxx::TrackMetadata& trackMetadata); + +private: + QFileInfo m_file; + SecurityTokenPointer m_pSecurityToken; +}; + +#endif // AUDIOTAGGER_H diff --git a/src/track/trackmetadata.cpp b/src/track/trackmetadata.cpp new file mode 100644 index 0000000000..aba6219bca --- /dev/null +++ b/src/track/trackmetadata.cpp @@ -0,0 +1,173 @@ +#include "track/trackmetadata.h" + +#include "util/time.h" + +namespace Mixxx { + +/*static*/ const double TrackMetadata::kBpmUndefined = 0.0; +/*static*/ const double TrackMetadata::kBpmMin = 0.0; // lower bound (exclusive) +/*static*/ const double TrackMetadata::kBpmMax = 300.0; // upper bound (inclusive) + +/*static*/ const int TrackMetadata::kCalendarYearInvalid = 0; + +double TrackMetadata::parseBpm(const QString& sBpm, bool* pValid) { + if (pValid) { + *pValid = false; + } + if (sBpm.trimmed().isEmpty()) { + return kBpmUndefined; + } + bool bpmValid = false; + double bpm = sBpm.toDouble(&bpmValid); + if (bpmValid) { + if (kBpmUndefined == bpm) { + // special case + if (pValid) { + *pValid = true; + } + return bpm; + } + while (kBpmMax < bpm) { + // Some applications might store the BPM as an + // integer scaled by a factor of 10 or 100 to + // preserve fractional digits. + qDebug() << "Scaling BPM value:" << bpm; + bpm /= 10.0; + } + if (isBpmValid(bpm)) { + if (pValid) { + *pValid = true; + } + return bpm; + } else { + qDebug() << "Invalid BPM value:" << sBpm << "->" << bpm; + } + } else { + qDebug() << "Failed to parse BPM:" << sBpm; + } + return kBpmUndefined; +} + +QString TrackMetadata::formatBpm(double bpm) { + if (isBpmValid(bpm)) { + return QString::number(bpm); + } else { + return QString(); + } +} + +QString TrackMetadata::formatBpm(int bpm) { + if (isBpmValid(bpm)) { + return QString::number(bpm); + } else { + return QString(); + } +} + +double TrackMetadata::normalizeBpm(double bpm) { + if (isBpmValid(bpm)) { + const double normalizedBpm = parseBpm(formatBpm(bpm)); + // NOTE(uklotzde): Subsequently formatting and parsing the + // normalized value should not alter it anymore! + DEBUG_ASSERT(normalizedBpm == parseBpm(formatBpm(normalizedBpm))); + return normalizedBpm; + } else { + return bpm; + } +} + +int TrackMetadata::parseCalendarYear(QString year, bool* pValid) { + const QDateTime dateTime(parseDateTime(year)); + if (0 < dateTime.date().year()) { + if (pValid) { + *pValid = true; + } + return dateTime.date().year(); + } else { + bool calendarYearValid = false; + // Ignore everything beginning with the first dash '-' + // to successfully parse the calendar year of incomplete + // dates like yyyy-MM or 2015-W07. + const QString calendarYearSection(year.section('-', 0, 0).trimmed()); + const int calendarYear = calendarYearSection.toInt(&calendarYearValid); + if (calendarYearValid) { + calendarYearValid = 0 < calendarYear; + } + if (pValid) { + *pValid = calendarYearValid; + } + if (calendarYearValid) { + return calendarYear; + } else { + return kCalendarYearInvalid; + } + } +} + +QString TrackMetadata::formatCalendarYear(QString year, bool* pValid) { + bool calendarYearValid = false; + int calendarYear = parseCalendarYear(year, &calendarYearValid); + if (pValid) { + *pValid = calendarYearValid; + } + if (calendarYearValid) { + return QString::number(calendarYear); + } else { + return QString(); // empty string + } +} + +QString TrackMetadata::reformatYear(QString year) { + const QDateTime dateTime(parseDateTime(year)); + if (dateTime.isValid()) { + // date/time + return formatDateTime(dateTime); + } + const QDate date(dateTime.date()); + if (date.isValid()) { + // only date + return formatDate(date); + } + bool calendarYearValid = false; + const QString calendarYear(formatCalendarYear(year, &calendarYearValid)); + if (calendarYearValid) { + // only calendar year + return calendarYear; + } + // just trim and simplify whitespaces + return year.simplified(); +} + +QString TrackMetadata::formatDuration(int duration) { + return Time::formatSeconds(duration, false); +} + +TrackMetadata::TrackMetadata() + : m_bpm(kBpmUndefined), + m_bitrate(0), + m_channels(0), + m_duration(0), + m_sampleRate(0) { +} + +bool operator==(const TrackMetadata& lhs, const TrackMetadata& rhs) { + return (lhs.getArtist() == rhs.getArtist()) && + (lhs.getTitle() == rhs.getTitle()) && + (lhs.getAlbum() == rhs.getAlbum()) && + (lhs.getAlbumArtist() == rhs.getAlbumArtist()) && + (lhs.getGenre() == rhs.getGenre()) && + (lhs.getComment() == rhs.getComment()) && + (lhs.getYear() == rhs.getYear()) && + (lhs.getTrackNumber() == rhs.getTrackNumber()) && + (lhs.getComposer() == rhs.getComposer()) && + (lhs.getGrouping() == rhs.getGrouping()) && + (lhs.getKey() == rhs.getKey()) && + (lhs.getChannels() == rhs.getChannels()) && + (lhs.getSampleRate() == rhs.getSampleRate()) && + (lhs.getBitrate() == rhs.getBitrate()) && + (lhs.getDuration() == rhs.getDuration()) && + (lhs.getBpm() == rhs.getBpm()) && + (lhs.getReplayGain() == rhs.getReplayGain()); +} + +} //namespace Mixxx diff --git a/src/track/trackmetadata.h b/src/track/trackmetadata.h new file mode 100644 index 0000000000..b802b0a628 --- /dev/null +++ b/src/track/trackmetadata.h @@ -0,0 +1,222 @@ +#ifndef MIXXX_TRACKMETADATA_H +#define MIXXX_TRACKMETADATA_H + +#include <QDateTime> + +#include "track/replaygain.h" + +namespace Mixxx { + +// DTO for track metadata properties. Must not be subclassed (no virtual destructor)! +class TrackMetadata { +public: + TrackMetadata(); + + inline const QString& getArtist() const { + return m_artist; + } + inline void setArtist(QString artist) { + m_artist = artist; + } + + inline const QString& getTitle() const { + return m_title; + } + inline void setTitle(QString title) { + m_title = title; + } + + inline const QString& getAlbum() const { + return m_album; + } + inline void setAlbum(QString album) { + m_album = album; + } + + inline const QString& getAlbumArtist() const { + return m_albumArtist; + } + inline void setAlbumArtist(QString albumArtist) { + m_albumArtist = albumArtist; + } + + inline const QString& getGenre() const { + return m_genre; + } + inline void setGenre(QString genre) { + m_genre = genre; + } + + inline const QString& getComment() const { + return m_comment; + } + inline void setComment(QString comment) { + m_comment = comment; + } + + // year, date or date/time formatted according to ISO 8601 + inline const QString& getYear() const { + return m_year; + } + inline void setYear(QString year) { + m_year = year; + } + + inline const QString& getTrackNumber() const { + return m_trackNumber; + } + inline void setTrackNumber(QString trackNumber) { + m_trackNumber = trackNumber; + } + + inline const QString& getComposer() const { + return m_composer; + } + inline void setComposer(QString composer) { + m_composer = composer; + } + + inline const QString& getGrouping() const { + return m_grouping; + } + inline void setGrouping(QString grouping) { + m_grouping = grouping; + } + + inline const QString& getKey() const { + return m_key; + } + inline void setKey(QString key) { + m_key = key; + } + + // #channels + inline int getChannels() const { + return m_channels; + } + inline void setChannels(int channels) { + m_channels = channels; + } + + // Hz + inline int getSampleRate() const { + return m_sampleRate; + } + inline void setSampleRate(int sampleRate) { + m_sampleRate = sampleRate; + } + + // kbit / s + inline int getBitrate() const { + return m_bitrate; + } + inline void setBitrate(int bitrate) { + m_bitrate = bitrate; + } + + // #seconds + inline int getDuration() const { + return m_duration; + } + inline void setDuration(int duration) { + m_duration = duration; + } + // Returns the duration as a string: H:MM:SS + static QString formatDuration(int duration); + + // beats / minute + static const double kBpmUndefined; + static const double kBpmMin; // lower bound (exclusive) + static const double kBpmMax; // upper bound (inclusive) + inline double getBpm() const { + return m_bpm; + } + inline int getBpmAsInteger() const { + return round(getBpm()); + } + inline static bool isBpmValid(double bpm) { + return (kBpmMin < bpm) && (kBpmMax >= bpm); + } + inline bool isBpmValid() const { + return isBpmValid(getBpm()); + } + inline void setBpm(double bpm) { + m_bpm = bpm; + } + inline void resetBpm() { + m_bpm = kBpmUndefined; + } + + const ReplayGain& getReplayGain() const { + return m_replayGain; + } + void setReplayGain(const ReplayGain& replayGain) { + m_replayGain = replayGain; + } + void resetReplayGain() { + m_replayGain = ReplayGain(); + } + + // Parse and format BPM metadata + static double parseBpm(const QString& sBpm, bool* pValid = 0); + static QString formatBpm(double bpm); + static QString formatBpm(int bpm); + static double normalizeBpm(double bpm); + + // Parse an format date/time values according to ISO 8601 + inline static QDate parseDate(QString str) { + return QDate::fromString(str.trimmed().replace(" ", ""), Qt::ISODate); + } + inline static QDateTime parseDateTime(QString str) { + return QDateTime::fromString(str.trimmed().replace(" ", ""), Qt::ISODate); + } + inline static QString formatDate(QDate date) { + return date.toString(Qt::ISODate); + } + inline static QString formatDateTime(QDateTime dateTime) { + return dateTime.toString(Qt::ISODate); + } + + // Parse and format the calendar year (for simplified display) + static const int kCalendarYearInvalid; + static int parseCalendarYear(QString year, bool* pValid = 0); + static QString formatCalendarYear(QString year, bool* pValid = 0); + + static QString reformatYear(QString year); + +private: + // String fields (in alphabetical order) + QString m_album; + QString m_albumArtist; + QString m_artist; + QString m_comment; + QString m_composer; + QString m_genre; + QString m_grouping; + QString m_key; + QString m_title; + QString m_trackNumber; + QString m_year; + + ReplayGain m_replayGain; + + // Floating-point fields (in alphabetical order) + double m_bpm; + + // Integer fields (in alphabetical order) + int m_bitrate; // kbit/s + int m_channels; + int m_duration; // seconds + int m_sampleRate; // Hz +}; + +bool operator==(const TrackMetadata& lhs, const TrackMetadata& rhs); + +inline +bool operator!=(const TrackMetadata& lhs, const TrackMetadata& rhs) { + return !(lhs == rhs); +} + +} + +#endif // MIXXX_TRACKMETADATA_H diff --git a/src/track/trackmetadatataglib.cpp b/src/track/trackmetadatataglib.cpp new file mode 100644 index 0000000000..f1e570b363 --- /dev/null +++ b/src/track/trackmetadatataglib.cpp @@ -0,0 +1,1364 @@ +#include "track/trackmetadatataglib.h" + +#include "util/assert.h" +#include "util/memory.h" + +// TagLib has support for the Ogg Opus file format since version 1.9 +#define TAGLIB_HAS_OPUSFILE \ + ((TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9))) + +// TagLib has support for hasID3v2Tag()/ID3v2Tag() for WAV files since version 1.9 +#define TAGLIB_HAS_WAV_ID3V2TAG \ + (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) + +// TagLib has full support for MP4 atom types since version 1.8 +#define TAGLIB_HAS_MP4_ATOM_TYPES \ + (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 8)) + +// TagLib has support for has<TagType>() style functions since version 1.9 +#define TAGLIB_HAS_TAG_CHECK \ + (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) + +#ifdef _WIN32 +static_assert(sizeof(wchar_t) == sizeof(QChar), "wchar_t is not the same size than QChar"); +#define TAGLIB_FILENAME_FROM_QSTRING(fileName) (const wchar_t*)fileName.utf16() +// Note: we cannot use QString::toStdWString since QT 4 is compiled with +// '/Zc:wchar_t-' flag and QT 5 not +#else +#define TAGLIB_FILENAME_FROM_QSTRING(fileName) (fileName).toLocal8Bit().constData() +#endif // _WIN32 + +#include <taglib/tfile.h> +#include <taglib/tmap.h> +#include <taglib/tstringlist.h> + +#include <taglib/mpegfile.h> +#include <taglib/mp4file.h> +#include <taglib/flacfile.h> +#include <taglib/vorbisfile.h> +#if TAGLIB_HAS_OPUSFILE +#include <taglib/opusfile.h> +#endif +#include <taglib/wavpackfile.h> +#include <taglib/wavfile.h> +#include <taglib/aifffile.h> + +#include <taglib/commentsframe.h> +#include <taglib/textidentificationframe.h> +#include <taglib/attachedpictureframe.h> +#include <taglib/flacpicture.h> + +namespace Mixxx { + +namespace { + +const QString kFileTypeAIFF("aiff"); +const QString kFileTypeFLAC("flac"); +const QString kFileTypeMP3("mp3"); +const QString kFileTypeMP4("mp4"); +const QString kFileTypeOggVorbis("ogg"); +const QString kFileTypeOggOpus("opus"); +const QString kFileTypeWAV("wav"); +const QString kFileTypeWavPack("wv"); + +inline bool hasID3v2Tag(TagLib::MPEG::File& file) { +#if TAGLIB_HAS_TAG_CHECK + return file.hasID3v2Tag(); +#else + return nullptr != file.ID3v2Tag(); +#endif +} + +inline bool hasAPETag(TagLib::MPEG::File& file) { +#if TAGLIB_HAS_TAG_CHECK + return file.hasAPETag(); +#else + return nullptr != file.APETag(); +#endif +} + +inline bool hasID3v2Tag(TagLib::FLAC::File& file) { +#if TAGLIB_HAS_TAG_CHECK + return file.hasID3v2Tag(); +#else + return nullptr != file.ID3v2Tag(); +#endif +} + +inline bool hasXiphComment(TagLib::FLAC::File& file) { +#if TAGLIB_HAS_TAG_CHECK + return file.hasXiphComment(); +#else + return nullptr != file.xiphComment(); +#endif +} + +inline bool hasAPETag(TagLib::WavPack::File& file) { +#if TAGLIB_HAS_TAG_CHECK + return file.hasAPETag(); +#else + return nullptr != file.APETag(); +#endif +} + +// Deduce the file type from the file name +QString getFileTypeFromFileName(QString fileName) { + DEBUG_ASSERT(!fileName.isEmpty()); + const QString fileType(fileName.section(".", -1).toLower().trimmed()); + if ("m4a" == fileType) { + return kFileTypeMP4; + } + if ("aif" == fileType) { + return kFileTypeAIFF; + } + return fileType; +} + +// http://id3.org/id3v2.3.0 +// "TYER: The 'Year' frame is a numeric string with a year of the +// recording. This frame is always four characters long (until +// the year 10000)." +const QString ID3V2_TYER_FORMAT("yyyy"); + +// http://id3.org/id3v2.3.0 +// "TDAT: The 'Date' frame is a numeric string in the DDMM +// format containing the date for the recording. This field +// is always four characters long." +const QString ID3V2_TDAT_FORMAT("ddMM"); + +// Taglib strings can be nullptr and using it could cause some segfaults, +// so in this case it will return a QString() +inline QString toQString(const TagLib::String& tString) { + if (tString.isNull()) { + return QString(); + } else { + return TStringToQString(tString); + } +} + +// Returns the first element of TagLib string list that is not empty. +QString toQStringFirstNotEmpty(const TagLib::StringList& strList) { + for (const auto& str: strList) { + if (!str.isEmpty()) { + return toQString(str); + } + } + return QString(); +} + +// Returns the text of an ID3v2 frame as a string. +inline QString toQString(const TagLib::ID3v2::Frame& frame) { + return toQString(frame.toString()); +} + +// Returns the first frame of an ID3v2 tag as a string. +QString toQStringFirstNotEmpty( + const TagLib::ID3v2::FrameList& frameList) { + for (const TagLib::ID3v2::Frame* pFrame: frameList) { + if (nullptr != pFrame) { + TagLib::String str(pFrame->toString()); + if (!str.isEmpty()) { + return toQString(str); + } + } + } + return QString(); +} + +// Returns the first non-empty value of an MP4 item as a string. +inline QString toQStringFirstNotEmpty(const TagLib::MP4::Item& mp4Item) { + return toQStringFirstNotEmpty(mp4Item.toStringList()); +} + +// Returns an APE item as a string. +inline QString toQString(const TagLib::APE::Item& apeItem) { + return toQString(apeItem.toString()); +} + +inline TagLib::String toTagLibString(const QString& str) { + const QByteArray qba(str.toUtf8()); + return TagLib::String(qba.constData(), TagLib::String::UTF8); +} + +inline bool parseBpm(TrackMetadata* pTrackMetadata, QString sBpm) { + DEBUG_ASSERT(pTrackMetadata); + + bool bpmValid = false; + double bpm = TrackMetadata::parseBpm(sBpm, &bpmValid); + if (bpmValid) { + pTrackMetadata->setBpm(bpm); + } + return bpmValid; +} + +inline QString formatTrackGain(const TrackMetadata& trackMetadata) { + const double trackGainRatio(trackMetadata.getReplayGain().getRatio()); + return ReplayGain::ratioToString(trackGainRatio); +} + +void parseTrackGain( + TrackMetadata* pTrackMetadata, + const QString& dbGain) { + DEBUG_ASSERT(pTrackMetadata); + + bool isRatioValid = false; + double ratio = ReplayGain::ratioFromString(dbGain, &isRatioValid); + if (isRatioValid) { + // Some applications (e.g. Rapid Evolution 3) write a replay gain + // of 0 dB even if the replay gain is undefined. To be safe we + // ignore this special value and instead prefer to recalculate + // the replay gain. + if (ratio == ReplayGain::kRatio0dB) { + // special case + qDebug() << "Ignoring possibly undefined gain:" << dbGain; + ratio = ReplayGain::kRatioUndefined; + } + ReplayGain replayGain(pTrackMetadata->getReplayGain()); + replayGain.setRatio(ratio); + pTrackMetadata->setReplayGain(replayGain); + } +} + +inline QString formatTrackPeak(const TrackMetadata& trackMetadata) { + const CSAMPLE trackGainPeak(trackMetadata.getReplayGain().getPeak()); + return ReplayGain::peakToString(trackGainPeak); +} + +void parseTrackPeak( + TrackMetadata* pTrackMetadata, + const QString& strPeak) { + DEBUG_ASSERT(pTrackMetadata); + + bool isPeakValid = false; + const CSAMPLE peak = ReplayGain::peakFromString(strPeak, &isPeakValid); + if (isPeakValid) { + ReplayGain replayGain(pTrackMetadata->getReplayGain()); + replayGain.setPeak(peak); + pTrackMetadata->setReplayGain(replayGain); + } +} + +void readAudioProperties(TrackMetadata* pTrackMetadata, + const TagLib::AudioProperties& audioProperties) { + DEBUG_ASSERT(pTrackMetadata); + + pTrackMetadata->setChannels(audioProperties.channels()); + pTrackMetadata->setSampleRate(audioProperties.sampleRate()); + pTrackMetadata->setDuration(audioProperties.length()); + pTrackMetadata->setBitrate(audioProperties.bitrate()); +} + +bool readAudioProperties(TrackMetadata* pTrackMetadata, + const TagLib::File& file) { + if (!file.isValid()) { + return false; + } + if (!pTrackMetadata) { + // implicitly successful + return true; + } + const TagLib::AudioProperties* pAudioProperties = + file.audioProperties(); + if (!pAudioProperties) { + qWarning() << "Failed to read audio properties from file" + << file.name(); + return false; + } + readAudioProperties(pTrackMetadata, *pAudioProperties); + return true; +} + +void readTrackMetadataFromTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag) { + if (!pTrackMetadata) { + return; // nothing to do + } + + pTrackMetadata->setTitle(toQString(tag.title())); + pTrackMetadata->setArtist(toQString(tag.artist())); + pTrackMetadata->setAlbum(toQString(tag.album())); + pTrackMetadata->setComment(toQString(tag.comment())); + pTrackMetadata->setGenre(toQString(tag.genre())); + + int iYear = tag.year(); + if (iYear > 0) { + pTrackMetadata->setYear(QString::number(iYear)); + } + + int iTrack = tag.track(); + if (iTrack > 0) { + pTrackMetadata->setTrackNumber(QString::number(iTrack)); + } +} + +// Workaround for missing const member function in TagLib +inline const TagLib::MP4::ItemListMap& getItemListMap(const TagLib::MP4::Tag& tag) { + return const_cast<TagLib::MP4::Tag&>(tag).itemListMap(); +} + +void readCoverArtFromID3v2Tag(QImage* pCoverArt, const TagLib::ID3v2::Tag& tag) { + if (!pCoverArt) { + return; // nothing to do + } + + TagLib::ID3v2::FrameList covertArtFrame = tag.frameListMap()["APIC"]; + if (!covertArtFrame.isEmpty()) { + TagLib::ID3v2::AttachedPictureFrame* picframe = + static_cast<TagLib::ID3v2::AttachedPictureFrame*>(covertArtFrame.front()); + TagLib::ByteVector data = picframe->picture(); + *pCoverArt = QImage::fromData( + reinterpret_cast<const uchar *>(data.data()), data.size()); + } +} + +void readCoverArtFromAPETag(QImage* pCoverArt, const TagLib::APE::Tag& tag) { + if (!pCoverArt) { + return; // nothing to do + } + + if (tag.itemListMap().contains("COVER ART (FRONT)")) { + const TagLib::ByteVector nullStringTerminator(1, 0); + TagLib::ByteVector item = + tag.itemListMap()["COVER ART (FRONT)"].value(); + int pos = item.find(nullStringTerminator); // skip the filename + if (++pos > 0) { + const TagLib::ByteVector& data = item.mid(pos); + *pCoverArt = QImage::fromData( + reinterpret_cast<const uchar *>(data.data()), data.size()); + } + } +} + +void readCoverArtFromXiphComment(QImage* pCoverArt, const TagLib::Ogg::XiphComment& tag) { + if (!pCoverArt) { + return; // nothing to do + } + + if (tag.fieldListMap().contains("METADATA_BLOCK_PICTURE")) { + QByteArray data( + QByteArray::fromBase64( + tag.fieldListMap()["METADATA_BLOCK_PICTURE"].front().toCString())); + TagLib::ByteVector tdata(data.data(), data.size()); + TagLib::FLAC::Picture p(tdata); + data = QByteArray(p.data().data(), p.data().size()); + *pCoverArt = QImage::fromData(data); + } else if (tag.fieldListMap().contains("COVERART")) { + QByteArray data( + QByteArray::fromBase64( + tag.fieldListMap()["COVERART"].toString().toCString())); + *pCoverArt = QImage::fromData(data); + } +} + +void readCoverArtFromMP4Tag(QImage* pCoverArt, const TagLib::MP4::Tag& tag) { + if (!pCoverArt) { + return; // nothing to do + } + + if (getItemListMap(tag).contains("covr")) { + TagLib::MP4::CoverArtList coverArtList = + getItemListMap(tag)["covr"].toCoverArtList(); + TagLib::ByteVector data = coverArtList.front().data(); + *pCoverArt = QImage::fromData( + reinterpret_cast<const uchar *>(data.data()), data.size()); + } +} + +void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) { + DEBUG_ASSERT(pTag); + + pTag->removeFrames(pFrame->frameID()); + pTag->addFrame(pFrame); +} + +TagLib::String::Type getID3v2StringType(const TagLib::ID3v2::Tag& tag, bool isNumericOrURL = false) { + TagLib::String::Type stringType; + // For an overview of the character encodings supported by + // the different ID3v2 versions please refer to the following + // resources: + // http://en.wikipedia.org/wiki/ID3#ID3v2 + // http://id3.org/id3v2.3.0 + // http://id3.org/id3v2.4.0-structure + if (4 <= tag.header()->majorVersion()) { + // For ID3v2.4.0 or higher prefer UTF-8, because it is a + // very compact representation for common cases and it is + // independent of the byte order. + stringType = TagLib::String::UTF8; + } else { + if (isNumericOrURL) { + // According to the ID3v2.3.0 specification: "All numeric + // strings and URLs are always encoded as ISO-8859-1." + stringType = TagLib::String::Latin1; + } else { + // For ID3v2.3.0 use UCS-2 (UTF-16 encoded Unicode with BOM) + // for arbitrary text, because UTF-8 and UTF-16BE are only + // supported since ID3v2.4.0 and the alternative ISO-8859-1 + // does not cover all Unicode characters. + stringType = TagLib::String::UTF16; + } + } + return stringType; +} + +// Finds the first comments frame with a matching description. +// If multiple comments frames with matching descriptions exist +// prefer the first with a non-empty content if requested. +TagLib::ID3v2::CommentsFrame* findFirstCommentsFrame( + const TagLib::ID3v2::Tag& tag, + const QString& description = QString(), + bool preferNotEmpty = true) { + TagLib::ID3v2::FrameList commentsFrames(tag.frameListMap()["COMM"]); + TagLib::ID3v2::CommentsFrame* pFirstFrame = nullptr; + for (TagLib::ID3v2::FrameList::ConstIterator it(commentsFrames.begin()); + it != commentsFrames.end(); ++it) { + auto pFrame = + dynamic_cast<TagLib::ID3v2::CommentsFrame*>(*it); + if (nullptr != pFrame) { + const QString frameDescription( + toQString(pFrame->description())); + if (0 == frameDescription.compare( + description, Qt::CaseInsensitive)) { + if (preferNotEmpty && pFrame->toString().isEmpty()) { + // we might need the first matching frame later + // even if it is empty + if (pFirstFrame == nullptr) { + pFirstFrame = pFrame; + } + } else { + // found what we are looking for + return pFrame; + } + } + } + } + // simply return the first matching frame + return pFirstFrame; +} + +// Finds the first text frame that with a matching description. +// If multiple comments frames with matching descriptions exist +// prefer the first with a non-empty content if requested. +TagLib::ID3v2::UserTextIdentificationFrame* findFirstUserTextIdentificationFrame( + const TagLib::ID3v2::Tag& tag, + const QString& description, + bool preferNotEmpty = true) { + DEBUG_ASSERT(!description.isEmpty()); + TagLib::ID3v2::FrameList textFrames(tag.frameListMap()["TXXX"]); + TagLib::ID3v2::UserTextIdentificationFrame* pFirstFrame = nullptr; + for (TagLib::ID3v2::FrameList::ConstIterator it(textFrames.begin()); + it != textFrames.end(); ++it) { + auto pFrame = + dynamic_cast<TagLib::ID3v2::UserTextIdentificationFrame*>(*it); + if (nullptr != pFrame) { + const QString frameDescription( + toQString(pFrame->description())); + if (0 == frameDescription.compare( + description, Qt::CaseInsensitive)) { + if (preferNotEmpty && pFrame->toString().isEmpty()) { + // we might need the first matching frame later + // even if it is empty + if (pFirstFrame == nullptr) { + pFirstFrame = pFrame; + } + } else { + // found what we are looking for + return pFrame; + } + } + } + } + // simply return the first matching frame + return pFirstFrame; +} + +void writeID3v2TextIdentificationFrame( + TagLib::ID3v2::Tag* pTag, + const TagLib::ByteVector &id, + const QString& text, + bool isNumericOrURL = false) { + DEBUG_ASSERT(pTag); + + const TagLib::String::Type stringType = + getID3v2StringType(*pTag, isNumericOrURL); + auto pFrame = + std::make_unique<TagLib::ID3v2::TextIdentificationFrame>(id, stringType); + pFrame->setText(toTagLibString(text)); + replaceID3v2Frame(pTag, pFrame.get()); + // Now the plain pointer in pFrame is owned and managed + // by pTag. We need to release the ownership to avoid + // double deletion! + pFrame.release(); +} + +void writeID3v2CommentsFrame( + TagLib::ID3v2::Tag* pTag, + const QString& text, + const QString& description = QString(), + bool isNumericOrURL = false) { + TagLib::ID3v2::CommentsFrame* pFrame = + findFirstCommentsFrame(*pTag, description); + if (nullptr != pFrame) { + // Modify existing frame + pFrame->setDescription(toTagLibString(description)); + pFrame->setText(toTagLibString(text)); + } else { + // Add a new frame + const TagLib::String::Type stringType = + getID3v2StringType(*pTag, isNumericOrURL); + auto pFrame = + std::make_unique<TagLib::ID3v2::CommentsFrame>(stringType); + pFrame->setDescription(toTagLibString(description)); + pFrame->setText(toTagLibString(text)); + pTag->addFrame(pFrame.get()); + // Now the plain pointer in pFrame is owned and managed + // by pTag. We need to release the ownership to avoid + // double deletion! + pFrame.release(); + } +} + +void writeID3v2UserTextIdentificationFrame( + TagLib::ID3v2::Tag* pTag, + const QString& text, + const QString& description, + bool isNumericOrURL = false) { + TagLib::ID3v2::UserTextIdentificationFrame* pFrame = + findFirstUserTextIdentificationFrame(*pTag, description); + if (nullptr != pFrame) { + // Modify existing frame + pFrame->setDescription(toTagLibString(description)); + pFrame->setText(toTagLibString(text)); + } else { + // Add a new frame + const TagLib::String::Type stringType = + getID3v2StringType(*pTag, isNumericOrURL); + auto pFrame = + std::make_unique<TagLib::ID3v2::UserTextIdentificationFrame>(stringType); + pFrame->setDescription(toTagLibString(description)); + pFrame->setText(toTagLibString(text)); + pTag->addFrame(pFrame.get()); + // Now the plain pointer in pFrame is owned and managed + // by pTag. We need to release the ownership to avoid + // double deletion! + pFrame.release(); + } +} + +void writeTrackMetadataIntoTag( + TagLib::Tag* pTag, |