1 /***************************************************************************
2  *   Copyright (C) 2008-2021 by Andrzej Rybczak                            *
3  *   andrzej@rybczak.net                                                   *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) any later version.                                   *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.              *
19  ***************************************************************************/
20 
21 #include "tags.h"
22 
23 #ifdef HAVE_TAGLIB_H
24 
25 // taglib includes
26 #include <id3v1tag.h>
27 #include <id3v2tag.h>
28 #include <fileref.h>
29 #include <flacfile.h>
30 #include <mpegfile.h>
31 #include <vorbisfile.h>
32 #include <tag.h>
33 #include <textidentificationframe.h>
34 #include <commentsframe.h>
35 #include <xiphcomment.h>
36 
37 #include <boost/filesystem.hpp>
38 #include "global.h"
39 #include "settings.h"
40 #include "utility/string.h"
41 #include "utility/wide_string.h"
42 
43 namespace {
44 
tagList(const MPD::MutableSong & s,MPD::Song::GetFunction f)45 TagLib::StringList tagList(const MPD::MutableSong &s, MPD::Song::GetFunction f)
46 {
47 	TagLib::StringList result;
48 	unsigned idx = 0;
49 	for (std::string value; !(value = (s.*f)(idx)).empty(); ++idx)
50 		result.append(ToWString(value));
51 	return result;
52 }
53 
readCommonTags(mpd_song * s,TagLib::Tag * tag)54 void readCommonTags(mpd_song *s, TagLib::Tag *tag)
55 {
56 	Tags::setAttribute(s, "Title", tag->title().to8Bit(true));
57 	Tags::setAttribute(s, "Artist", tag->artist().to8Bit(true));
58 	Tags::setAttribute(s, "Album", tag->album().to8Bit(true));
59 	Tags::setAttribute(s, "Date", boost::lexical_cast<std::string>(tag->year()));
60 	Tags::setAttribute(s, "Track", boost::lexical_cast<std::string>(tag->track()));
61 	Tags::setAttribute(s, "Genre", tag->genre().to8Bit(true));
62 	Tags::setAttribute(s, "Comment", tag->comment().to8Bit(true));
63 }
64 
readID3v1Tags(mpd_song * s,TagLib::ID3v1::Tag * tag)65 void readID3v1Tags(mpd_song *s, TagLib::ID3v1::Tag *tag)
66 {
67 	readCommonTags(s, tag);
68 }
69 
readID3v2Tags(mpd_song * s,TagLib::ID3v2::Tag * tag)70 void readID3v2Tags(mpd_song *s, TagLib::ID3v2::Tag *tag)
71 {
72 	auto readFrame = [s](const TagLib::ID3v2::FrameList &fields, const char *name) {
73 		for (const auto &field : fields)
74 		{
75 			if (auto textFrame = dynamic_cast<TagLib::ID3v2::TextIdentificationFrame *>(field))
76 			{
77 				auto values = textFrame->fieldList();
78 				for (const auto &value : values)
79 					Tags::setAttribute(s, name, value.to8Bit(true));
80 			}
81 			else
82 				Tags::setAttribute(s, name, field->toString().to8Bit(true));
83 		}
84 	};
85 	auto &frames = tag->frameListMap();
86 	readFrame(frames["TIT2"], "Title");
87 	readFrame(frames["TPE1"], "Artist");
88 	readFrame(frames["TPE2"], "AlbumArtist");
89 	readFrame(frames["TALB"], "Album");
90 	readFrame(frames["TDRC"], "Date");
91 	readFrame(frames["TRCK"], "Track");
92 	readFrame(frames["TCON"], "Genre");
93 	readFrame(frames["TCOM"], "Composer");
94 	readFrame(frames["TPE3"], "Performer");
95 	readFrame(frames["TPOS"], "Disc");
96 	readFrame(frames["COMM"], "Comment");
97 }
98 
readXiphComments(mpd_song * s,TagLib::Ogg::XiphComment * tag)99 void readXiphComments(mpd_song *s, TagLib::Ogg::XiphComment *tag)
100 {
101 	auto readField = [s](const TagLib::StringList &fields, const char *name) {
102 		for (const auto &field : fields)
103 			Tags::setAttribute(s, name, field.to8Bit(true));
104 	};
105 	auto &fields = tag->fieldListMap();
106 	readField(fields["TITLE"], "Title");
107 	readField(fields["ARTIST"], "Artist");
108 	readField(fields["ALBUMARTIST"], "AlbumArtist");
109 	readField(fields["ALBUM"], "Album");
110 	readField(fields["DATE"], "Date");
111 	readField(fields["TRACKNUMBER"], "Track");
112 	readField(fields["GENRE"], "Genre");
113 	readField(fields["COMPOSER"], "Composer");
114 	readField(fields["PERFORMER"], "Performer");
115 	readField(fields["DISCNUMBER"], "Disc");
116 	readField(fields["COMMENT"], "Comment");
117 }
118 
writeCommonTags(const MPD::MutableSong & s,TagLib::Tag * tag)119 void writeCommonTags(const MPD::MutableSong &s, TagLib::Tag *tag)
120 {
121 	tag->setTitle(ToWString(s.getTitle()));
122 	tag->setArtist(ToWString(s.getArtist()));
123 	tag->setAlbum(ToWString(s.getAlbum()));
124 	try {
125 		tag->setYear(boost::lexical_cast<TagLib::uint>(s.getDate()));
126 	} catch (boost::bad_lexical_cast &) {
127 		std::cerr << "writeCommonTags: couldn't write 'year' tag to '" << s.getURI() << "' as it's not a positive integer\n";
128 	}
129 	try {
130 		tag->setTrack(boost::lexical_cast<TagLib::uint>(s.getTrack()));
131 	} catch (boost::bad_lexical_cast &) {
132 		std::cerr << "writeCommonTags: couldn't write 'track' tag to '" << s.getURI() << "' as it's not a positive integer\n";
133 	}
134 	tag->setGenre(ToWString(s.getGenre()));
135 	tag->setComment(ToWString(s.getComment()));
136 }
137 
writeID3v2Tags(const MPD::MutableSong & s,TagLib::ID3v2::Tag * tag)138 void writeID3v2Tags(const MPD::MutableSong &s, TagLib::ID3v2::Tag *tag)
139 {
140 	auto writeID3v2 = [&](const TagLib::ByteVector &type, const TagLib::StringList &list) {
141 		tag->removeFrames(type);
142 		if (!list.isEmpty())
143 		{
144 			if (type == "COMM") // comment needs to be handled separately
145 			{
146 				auto frame = new TagLib::ID3v2::CommentsFrame(TagLib::String::UTF8);
147 				// apparently there can't be multiple comments,
148 				// so if there is more than one, join them.
149 				frame->setText(join(list, TagLib::String(MPD::Song::TagsSeparator, TagLib::String::UTF8)));
150 				tag->addFrame(frame);
151 			}
152 			else
153 			{
154 				auto frame = new TagLib::ID3v2::TextIdentificationFrame(type, TagLib::String::UTF8);
155 				frame->setText(list);
156 				tag->addFrame(frame);
157 			}
158 		}
159 	};
160 	writeID3v2("TIT2", tagList(s, &MPD::Song::getTitle));
161 	writeID3v2("TPE1", tagList(s, &MPD::Song::getArtist));
162 	writeID3v2("TPE2", tagList(s, &MPD::Song::getAlbumArtist));
163 	writeID3v2("TALB", tagList(s, &MPD::Song::getAlbum));
164 	writeID3v2("TDRC", tagList(s, &MPD::Song::getDate));
165 	writeID3v2("TRCK", tagList(s, &MPD::Song::getTrack));
166 	writeID3v2("TCON", tagList(s, &MPD::Song::getGenre));
167 	writeID3v2("TCOM", tagList(s, &MPD::Song::getComposer));
168 	writeID3v2("TPE3", tagList(s, &MPD::Song::getPerformer));
169 	writeID3v2("TPOS", tagList(s, &MPD::Song::getDisc));
170 	writeID3v2("COMM", tagList(s, &MPD::Song::getComment));
171 }
172 
writeXiphComments(const MPD::MutableSong & s,TagLib::Ogg::XiphComment * tag)173 void writeXiphComments(const MPD::MutableSong &s, TagLib::Ogg::XiphComment *tag)
174 {
175 	auto writeXiph = [&](const TagLib::String &type, const TagLib::StringList &list) {
176 		for (auto it = list.begin(); it != list.end(); ++it)
177 			tag->addField(type, *it);
178 	};
179 	// remove field previously used as album artist
180 	tag->removeFields("ALBUM ARTIST");
181 	// remove field TRACK, some taggers use it as TRACKNUMBER
182 	tag->removeFields("TRACK");
183 	// remove field DISC, some taggers use it as DISCNUMBER
184 	tag->removeFields("DISC");
185 	// remove field DESCRIPTION, it's displayed as COMMENT
186 	tag->removeFields("DESCRIPTION");
187 	writeXiph("TITLE", tagList(s, &MPD::Song::getTitle));
188 	writeXiph("ARTIST", tagList(s, &MPD::Song::getArtist));
189 	writeXiph("ALBUMARTIST", tagList(s, &MPD::Song::getAlbumArtist));
190 	writeXiph("ALBUM", tagList(s, &MPD::Song::getAlbum));
191 	writeXiph("DATE", tagList(s, &MPD::Song::getDate));
192 	writeXiph("TRACKNUMBER", tagList(s, &MPD::Song::getTrack));
193 	writeXiph("GENRE", tagList(s, &MPD::Song::getGenre));
194 	writeXiph("COMPOSER", tagList(s, &MPD::Song::getComposer));
195 	writeXiph("PERFORMER", tagList(s, &MPD::Song::getPerformer));
196 	writeXiph("DISCNUMBER", tagList(s, &MPD::Song::getDisc));
197 	writeXiph("COMMENT", tagList(s, &MPD::Song::getComment));
198 }
199 
getReplayGain(TagLib::Ogg::XiphComment * tag)200 Tags::ReplayGainInfo getReplayGain(TagLib::Ogg::XiphComment *tag)
201 {
202 	auto first_or_empty = [](const TagLib::StringList &list) {
203 		std::string result;
204 		if (!list.isEmpty())
205 			result = list.front().to8Bit(true);
206 		return result;
207 	};
208 	auto &fields = tag->fieldListMap();
209 	return Tags::ReplayGainInfo(
210 		first_or_empty(fields["REPLAYGAIN_REFERENCE_LOUDNESS"]),
211 		first_or_empty(fields["REPLAYGAIN_TRACK_GAIN"]),
212 		first_or_empty(fields["REPLAYGAIN_TRACK_PEAK"]),
213 		first_or_empty(fields["REPLAYGAIN_ALBUM_GAIN"]),
214 		first_or_empty(fields["REPLAYGAIN_ALBUM_PEAK"])
215 	);
216 }
217 
218 }
219 
220 namespace Tags {
221 
setAttribute(mpd_song * s,const char * name,const std::string & value)222 void setAttribute(mpd_song *s, const char *name, const std::string &value)
223 {
224 	mpd_pair pair = { name, value.c_str() };
225 	mpd_song_feed(s, &pair);
226 }
227 
extendedSetSupported(const TagLib::File * f)228 bool extendedSetSupported(const TagLib::File *f)
229 {
230 	return dynamic_cast<const TagLib::MPEG::File *>(f)
231 	||     dynamic_cast<const TagLib::Ogg::Vorbis::File *>(f)
232 	||     dynamic_cast<const TagLib::FLAC::File *>(f);
233 }
234 
readReplayGain(TagLib::File * f)235 ReplayGainInfo readReplayGain(TagLib::File *f)
236 {
237 	ReplayGainInfo result;
238 	if (auto ogg_file = dynamic_cast<TagLib::Ogg::Vorbis::File *>(f))
239 	{
240 		if (auto xiph = ogg_file->tag())
241 			result = getReplayGain(xiph);
242 	}
243 	else if (auto flac_file = dynamic_cast<TagLib::FLAC::File *>(f))
244 	{
245 		if (auto xiph = flac_file->xiphComment())
246 			result = getReplayGain(xiph);
247 	}
248 	return result;
249 }
250 
read(mpd_song * s)251 void read(mpd_song *s)
252 {
253 	TagLib::FileRef f(mpd_song_get_uri(s));
254 	if (f.isNull())
255 		return;
256 
257 	setAttribute(s, "Time", boost::lexical_cast<std::string>(f.audioProperties()->length()));
258 
259 	if (auto mpeg_file = dynamic_cast<TagLib::MPEG::File *>(f.file()))
260 	{
261 		// prefer id3v2 only if available
262 		if (auto id3v2 = mpeg_file->ID3v2Tag())
263 			readID3v2Tags(s, id3v2);
264 		else if (auto id3v1 = mpeg_file->ID3v1Tag())
265 			readID3v1Tags(s, id3v1);
266 	}
267 	else if (auto ogg_file = dynamic_cast<TagLib::Ogg::Vorbis::File *>(f.file()))
268 	{
269 		if (auto xiph = ogg_file->tag())
270 			readXiphComments(s, xiph);
271 	}
272 	else if (auto flac_file = dynamic_cast<TagLib::FLAC::File *>(f.file()))
273 	{
274 		if (auto xiph = flac_file->xiphComment())
275 			readXiphComments(s, xiph);
276 	}
277 	else
278 		readCommonTags(s, f.tag());
279 }
280 
write(MPD::MutableSong & s)281 bool write(MPD::MutableSong &s)
282 {
283 	std::string old_name;
284 	if (s.isFromDatabase())
285 		old_name += Config.mpd_music_dir;
286 	old_name += s.getURI();
287 
288 	TagLib::FileRef f(old_name.c_str());
289 	if (f.isNull())
290 		return false;
291 
292 	bool saved = false;
293 	if (auto mpeg_file = dynamic_cast<TagLib::MPEG::File *>(f.file()))
294 	{
295 		writeID3v2Tags(s, mpeg_file->ID3v2Tag(true));
296 		// write id3v2.4 tags only
297 		if (!mpeg_file->save(TagLib::MPEG::File::ID3v2, true, 4, false))
298 			return false;
299 		// do not call generic save() as it will duplicate tags
300 		saved = true;
301 	}
302 	else if (auto ogg_file = dynamic_cast<TagLib::Ogg::Vorbis::File *>(f.file()))
303 	{
304 		writeXiphComments(s, ogg_file->tag());
305 	}
306 	else if (auto flac_file = dynamic_cast<TagLib::FLAC::File *>(f.file()))
307 	{
308 		writeXiphComments(s, flac_file->xiphComment(true));
309 	}
310 	else
311 		writeCommonTags(s, f.tag());
312 
313 	if (!saved && !f.save())
314 		return false;
315 
316 	// TODO: move this somewhere else
317 	if (!s.getNewName().empty())
318 	{
319 		std::string new_name;
320 		if (s.isFromDatabase())
321 			new_name += Config.mpd_music_dir;
322 		new_name += s.getDirectory();
323 		new_name += "/";
324 		new_name += s.getNewName();
325 		boost::filesystem::rename(old_name, new_name);
326 	}
327 	return true;
328 }
329 
330 }
331 
332 #endif // HAVE_TAGLIB_H
333