summaryrefslogtreecommitdiffstats
path: root/src/track
diff options
context:
space:
mode:
authorUwe Klotz <uwe_klotz@web.de>2015-12-27 23:27:43 +0100
committerUwe Klotz <uwe_klotz@web.de>2015-12-30 23:32:16 +0100
commitbf14e1d6c090f0f45d829b48f9b5e947e82e6cd4 (patch)
tree0f30d53de07e39eb4546e96edacad30c12769a00 /src/track
parente2ae5ca18d3ad7e477dc9644b5ff1355c747a065 (diff)
Merge metadata/ into track/
Diffstat (limited to 'src/track')
-rw-r--r--src/track/audiotagger.cpp28
-rw-r--r--src/track/audiotagger.h22
-rw-r--r--src/track/trackmetadata.cpp173
-rw-r--r--src/track/trackmetadata.h222
-rw-r--r--src/track/trackmetadatataglib.cpp1364
-rw-r--r--src/track/trackmetadatataglib.h38
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,