/***************************************************************************
* Copyright (C) 2008-2021 by Andrzej Rybczak *
* [email protected] *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
***************************************************************************/
#include "tags.h"
#ifdef HAVE_TAGLIB_H
// taglib includes
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "global.h"
#include "settings.h"
#include "utility/string.h"
#include "utility/wide_string.h"
namespace {
TagLib::StringList tagList(const MPD::MutableSong &s, MPD::Song::GetFunction f)
{
TagLib::StringList result;
unsigned idx = 0;
for (std::string value; !(value = (s.*f)(idx)).empty(); ++idx)
result.append(ToWString(value));
return result;
}
void readCommonTags(mpd_song *s, TagLib::Tag *tag)
{
Tags::setAttribute(s, "Title", tag->title().to8Bit(true));
Tags::setAttribute(s, "Artist", tag->artist().to8Bit(true));
Tags::setAttribute(s, "Album", tag->album().to8Bit(true));
Tags::setAttribute(s, "Date", boost::lexical_cast<:string>(tag->year()));
Tags::setAttribute(s, "Track", boost::lexical_cast<:string>(tag->track()));
Tags::setAttribute(s, "Genre", tag->genre().to8Bit(true));
Tags::setAttribute(s, "Comment", tag->comment().to8Bit(true));
}
void readID3v1Tags(mpd_song *s, TagLib::ID3v1::Tag *tag)
{
readCommonTags(s, tag);
}
void readID3v2Tags(mpd_song *s, TagLib::ID3v2::Tag *tag)
{
auto readFrame = [s](const TagLib::ID3v2::FrameList &fields, const char *name) {
for (const auto &field : fields)
{
if (auto textFrame = dynamic_cast<:id3v2::textidentificationframe>(field))
{
auto values = textFrame->fieldList();
for (const auto &value : values)
Tags::setAttribute(s, name, value.to8Bit(true));
}
else
Tags::setAttribute(s, name, field->toString().to8Bit(true));
}
};
auto &frames = tag->frameListMap();
readFrame(frames["TIT2"], "Title");
readFrame(frames["TPE1"], "Artist");
readFrame(frames["TPE2"], "AlbumArtist");
readFrame(frames["TALB"], "Album");
readFrame(frames["TDRC"], "Date");
readFrame(frames["TRCK"], "Track");
readFrame(frames["TCON"], "Genre");
readFrame(frames["TCOM"], "Composer");
readFrame(frames["TPE4"], "Performer");
readFrame(frames["TPOS"], "Disc");
readFrame(frames["COMM"], "Comment");
}
void readXiphComments(mpd_song *s, TagLib::Ogg::XiphComment *tag)
{
auto readField = [s](const TagLib::StringList &fields, const char *name) {
for (const auto &field : fields)
Tags::setAttribute(s, name, field.to8Bit(true));
};
auto &fields = tag->fieldListMap();
readField(fields["TITLE"], "Title");
readField(fields["ARTIST"], "Artist");
readField(fields["ALBUMARTIST"], "AlbumArtist");
readField(fields["ALBUM"], "Album");
readField(fields["DATE"], "Date");
readField(fields["TRACKNUMBER"], "Track");
readField(fields["GENRE"], "Genre");
readField(fields["COMPOSER"], "Composer");
readField(fields["PERFORMER"], "Performer");
readField(fields["DISCNUMBER"], "Disc");
readField(fields["COMMENT"], "Comment");
}
void writeCommonTags(const MPD::MutableSong &s, TagLib::Tag *tag)
{
tag->setTitle(ToWString(s.getTitle()));
tag->setArtist(ToWString(s.getArtist()));
tag->setAlbum(ToWString(s.getAlbum()));
try {
tag->setYear(boost::lexical_cast(s.getDate()));
} catch (boost::bad_lexical_cast &) {
std::cerr << "writeCommonTags: couldn't write 'year' tag to '" << s.getURI() << "' as it's not a positive integer\n";
}
try {
tag->setTrack(boost::lexical_cast(s.getTrack()));
} catch (boost::bad_lexical_cast &) {
std::cerr << "writeCommonTags: couldn't write 'track' tag to '" << s.getURI() << "' as it's not a positive integer\n";
}
tag->setGenre(ToWString(s.getGenre()));
tag->setComment(ToWString(s.getComment()));
}
void writeID3v2Tags(const MPD::MutableSong &s, TagLib::ID3v2::Tag *tag)
{
auto writeID3v2 = [&](const TagLib::ByteVector &type, const TagLib::StringList &list) {
tag->removeFrames(type);
if (!list.isEmpty())
{
if (type == "COMM") // comment needs to be handled separately
{
auto frame = new TagLib::ID3v2::CommentsFrame(TagLib::String::UTF8);
// apparently there can't be multiple comments,
// so if there is more than one, join them.
frame->setText(join(list, TagLib::String(MPD::Song::TagsSeparator, TagLib::String::UTF8)));
tag->addFrame(frame);
}
else
{
auto frame = new TagLib::ID3v2::TextIdentificationFrame(type, TagLib::String::UTF8);
frame->setText(list);
tag->addFrame(frame);
}
}
};
writeID3v2("TIT2", tagList(s, &MPD::Song::getTitle));
writeID3v2("TPE1", tagList(s, &MPD::Song::getArtist));
writeID3v2("TPE2", tagList(s, &MPD::Song::getAlbumArtist));
writeID3v2("TALB", tagList(s, &MPD::Song::getAlbum));
writeID3v2("TDRC", tagList(s, &MPD::Song::getDate));
writeID3v2("TRCK", tagList(s, &MPD::Song::getTrack));
writeID3v2("TCON", tagList(s, &MPD::Song::getGenre));
writeID3v2("TCOM", tagList(s, &MPD::Song::getComposer));
writeID3v2("TPE4", tagList(s, &MPD::Song::getPerformer));
writeID3v2("TPOS", tagList(s, &MPD::Song::getDisc));
writeID3v2("COMM", tagList(s, &MPD::Song::getComment));
}
void writeXiphComments(const MPD::MutableSong &s, TagLib::Ogg::XiphComment *tag)
{
auto writeXiph = [&](const TagLib::String &type, const TagLib::StringList &list) {
tag->removeFields(type);
for (auto it = list.begin(); it != list.end(); ++it)
tag->addField(type, *it, it == list.begin());
};
// remove field previously used as album artist
tag->removeFields("ALBUM ARTIST");
// remove field TRACK, some taggers use it as TRACKNUMBER
tag->removeFields("TRACK");
// remove field DISC, some taggers use it as DISCNUMBER
tag->removeFields("DISC");
// remove field DESCRIPTION, it's displayed as COMMENT
tag->removeFields("DESCRIPTION");
writeXiph("TITLE", tagList(s, &MPD::Song::getTitle));
writeXiph("ARTIST", tagList(s, &MPD::Song::getArtist));
writeXiph("ALBUMARTIST", tagList(s, &MPD::Song::getAlbumArtist));
writeXiph("ALBUM", tagList(s, &MPD::Song::getAlbum));
writeXiph("DATE", tagList(s, &MPD::Song::getDate));
writeXiph("TRACKNUMBER", tagList(s, &MPD::Song::getTrack));
writeXiph("GENRE", tagList(s, &MPD::Song::getGenre));
writeXiph("COMPOSER", tagList(s, &MPD::Song::getComposer));
writeXiph("PERFORMER", tagList(s, &MPD::Song::getPerformer));
writeXiph("DISCNUMBER", tagList(s, &MPD::Song::getDisc));
writeXiph("COMMENT", tagList(s, &MPD::Song::getComment));
}
Tags::ReplayGainInfo getReplayGain(TagLib::Ogg::XiphComment *tag)
{
auto first_or_empty = [](const TagLib::StringList &list) {
std::string result;
if (!list.isEmpty())
result = list.front().to8Bit(true);
return result;
};
auto &fields = tag->fieldListMap();
return Tags::ReplayGainInfo(
first_or_empty(fields["REPLAYGAIN_REFERENCE_LOUDNESS"]),
first_or_empty(fields["REPLAYGAIN_TRACK_GAIN"]),
first_or_empty(fields["REPLAYGAIN_TRACK_PEAK"]),
first_or_empty(fields["REPLAYGAIN_ALBUM_GAIN"]),
first_or_empty(fields["REPLAYGAIN_ALBUM_PEAK"])
);
}
}
namespace Tags {
void setAttribute(mpd_song *s, const char *name, const std::string &value)
{
mpd_pair pair = { name, value.c_str() };
mpd_song_feed(s, &pair);
}
bool extendedSetSupported(const TagLib::File *f)
{
return dynamic_cast(f)
|| dynamic_cast(f)
|| dynamic_cast(f)
|| dynamic_cast(f);
}
ReplayGainInfo readReplayGain(TagLib::File *f)
{
ReplayGainInfo result;
if (auto vorbis_file = dynamic_cast<:ogg::vorbis::file>(f))
{
if (auto xiph = vorbis_file->tag())
result = getReplayGain(xiph);
}
else if (auto opus_file = dynamic_cast<:ogg::opus::file>(f))
{
if (auto xiph = opus_file->tag())
result = getReplayGain(xiph);
}
else if (auto flac_file = dynamic_cast<:flac::file>(f))
{
if (auto xiph = flac_file->xiphComment())
result = getReplayGain(xiph);
}
return result;
}
void read(mpd_song *s)
{
TagLib::FileRef f(mpd_song_get_uri(s));
if (f.isNull())
return;
setAttribute(s, "Time", boost::lexical_cast<:string>(f.audioProperties()->lengthInSeconds()));
if (auto mpeg_file = dynamic_cast<:mpeg::file>(f.file()))
{
// prefer id3v2 only if available
if (auto id3v2 = mpeg_file->ID3v2Tag())
readID3v2Tags(s, id3v2);
else if (auto id3v1 = mpeg_file->ID3v1Tag())
readID3v1Tags(s, id3v1);
}
else if (auto vorbis_file = dynamic_cast<:ogg::vorbis::file>(f.file()))
{
if (auto xiph = vorbis_file->tag())
readXiphComments(s, xiph);
}
else if (auto opus_file = dynamic_cast<:ogg::opus::file>(f.file()))
{
if (auto xiph = opus_file->tag())
readXiphComments(s, xiph);
}
else if (auto flac_file = dynamic_cast<:flac::file>(f.file()))
{
if (auto xiph = flac_file->xiphComment())
readXiphComments(s, xiph);
}
else
readCommonTags(s, f.tag());
}
bool write(MPD::MutableSong &s)
{
std::string old_name;
if (s.isFromDatabase())
old_name += Config.mpd_music_dir;
old_name += s.getURI();
TagLib::FileRef f(old_name.c_str());
if (f.isNull())
return false;
if (auto mpeg_file = dynamic_cast<:mpeg::file>(f.file()))
{
writeID3v2Tags(s, mpeg_file->ID3v2Tag(true));
}
else if (auto vorbis_file = dynamic_cast<:ogg::vorbis::file>(f.file()))
{
writeXiphComments(s, vorbis_file->tag());
}
else if (auto opus_file = dynamic_cast<:ogg::opus::file>(f.file()))
{
writeXiphComments(s, opus_file->tag());
}
else if (auto flac_file = dynamic_cast<:flac::file>(f.file()))
{
writeXiphComments(s, flac_file->xiphComment(true));
}
else
writeCommonTags(s, f.tag());
if (!f.save())
return false;
// TODO: move this somewhere else
if (!s.getNewName().empty())
{
std::string new_name;
if (s.isFromDatabase())
new_name += Config.mpd_music_dir;
new_name += s.getDirectory();
new_name += "/";
new_name += s.getNewName();
boost::filesystem::rename(old_name, new_name);
}
return true;
}
}
#endif // HAVE_TAGLIB_H