TagId3.cxx 8.95 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2017 The Music Player Daemon Project
3
 * http://www.musicpd.org
4 5 6 7 8 9 10 11 12 13
 *
 * 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.
14 15 16 17
 *
 * 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 19
 */

20
#include "config.h"
Max Kellermann's avatar
Max Kellermann committed
21
#include "TagId3.hxx"
22
#include "Id3Load.hxx"
23
#include "Id3MusicBrainz.hxx"
24
#include "TagHandler.hxx"
25
#include "TagTable.hxx"
26
#include "TagBuilder.hxx"
27
#include "util/Alloc.hxx"
28
#include "util/ScopeExit.hxx"
29
#include "util/StringUtil.hxx"
30
#include "Log.hxx"
Max Kellermann's avatar
Max Kellermann committed
31

32 33
#include <id3tag.h>

34
#include <string>
35
#include <stdexcept>
36

37
#include <string.h>
38
#include <stdlib.h>
39

40 41 42 43 44 45 46
#  ifndef ID3_FRAME_COMPOSER
#    define ID3_FRAME_COMPOSER "TCOM"
#  endif
#  ifndef ID3_FRAME_DISC
#    define ID3_FRAME_DISC "TPOS"
#  endif

Bart Nagel's avatar
Bart Nagel committed
47 48 49 50
#ifndef ID3_FRAME_ARTIST_SORT
#define ID3_FRAME_ARTIST_SORT "TSOP"
#endif

51
#ifndef ID3_FRAME_ALBUM_ARTIST_SORT
Bart Nagel's avatar
Bart Nagel committed
52
#define ID3_FRAME_ALBUM_ARTIST_SORT "TSO2" /* this one is unofficial, introduced by Itunes */
53 54 55 56 57 58
#endif

#ifndef ID3_FRAME_ALBUM_ARTIST
#define ID3_FRAME_ALBUM_ARTIST "TPE2"
#endif

59
gcc_pure
60
static id3_utf8_t *
61
tag_id3_getstring(const struct id3_frame *frame, unsigned i) noexcept
62
{
63
	id3_field *field = id3_frame_field(frame, i);
Max Kellermann's avatar
Max Kellermann committed
64 65
	if (field == nullptr)
		return nullptr;
66

67
	const id3_ucs4_t *ucs4 = id3_field_getstring(field);
Max Kellermann's avatar
Max Kellermann committed
68 69
	if (ucs4 == nullptr)
		return nullptr;
70 71 72 73

	return id3_ucs4_utf8duplicate(ucs4);
}

74 75
/* This will try to convert a string to utf-8,
 */
Max Kellermann's avatar
Max Kellermann committed
76
static id3_utf8_t *
77
import_id3_string(const id3_ucs4_t *ucs4)
78
{
79 80 81
	id3_utf8_t *utf8 = id3_ucs4_utf8duplicate(ucs4);
	if (gcc_unlikely(utf8 == nullptr))
		return nullptr;
82

83
	AtScopeExit(utf8) { free(utf8); };
84

85
	return (id3_utf8_t *)xstrdup(Strip((char *)utf8));
86 87
}

88 89 90 91 92 93 94
/**
 * Import a "Text information frame" (ID3v2.4.0 section 4.2).  It
 * contains 2 fields:
 *
 * - encoding
 * - string list
 */
95
static void
96
tag_id3_import_text_frame(const struct id3_frame *frame,
97
			  TagType type,
98
			  const TagHandler &handler, void *handler_ctx)
99
{
100
	if (frame->nfields != 2)
101
		return;
102 103 104

	/* check the encoding field */

105
	const id3_field *field = id3_frame_field(frame, 0);
Max Kellermann's avatar
Max Kellermann committed
106
	if (field == nullptr || field->type != ID3_FIELD_TYPE_TEXTENCODING)
107
		return;
108

109 110 111
	/* process the value(s) */

	field = id3_frame_field(frame, 1);
Max Kellermann's avatar
Max Kellermann committed
112
	if (field == nullptr || field->type != ID3_FIELD_TYPE_STRINGLIST)
113 114 115
		return;

	/* Get the number of strings available */
116 117 118
	const unsigned nstrings = id3_field_getnstrings(field);
	for (unsigned i = 0; i < nstrings; i++) {
		const id3_ucs4_t *ucs4 = id3_field_getstrings(field, i);
Max Kellermann's avatar
Max Kellermann committed
119
		if (ucs4 == nullptr)
120 121
			continue;

122
		if (type == TAG_GENRE)
123 124
			ucs4 = id3_genre_name(ucs4);

125
		id3_utf8_t *utf8 = import_id3_string(ucs4);
Max Kellermann's avatar
Max Kellermann committed
126
		if (utf8 == nullptr)
127 128
			continue;

129 130
		AtScopeExit(utf8) { free(utf8); };

131 132
		tag_handler_invoke_tag(handler, handler_ctx,
				       type, (const char *)utf8);
133
	}
134 135
}

136 137 138 139 140
/**
 * Import all text frames with the specified id (ID3v2.4.0 section
 * 4.2).  This is a wrapper for tag_id3_import_text_frame().
 */
static void
141
tag_id3_import_text(struct id3_tag *tag, const char *id, TagType type,
142
		    const TagHandler &handler, void *handler_ctx)
143 144 145
{
	const struct id3_frame *frame;
	for (unsigned i = 0;
Max Kellermann's avatar
Max Kellermann committed
146
	     (frame = id3_tag_findframe(tag, id, i)) != nullptr; ++i)
147
		tag_id3_import_text_frame(frame, type,
148
					  handler, handler_ctx);
149 150
}

151 152 153 154 155 156 157 158 159 160
/**
 * Import a "Comment frame" (ID3v2.4.0 section 4.10).  It
 * contains 4 fields:
 *
 * - encoding
 * - language
 * - string
 * - full string (we use this one)
 */
static void
161
tag_id3_import_comment_frame(const struct id3_frame *frame, TagType type,
162
			     const TagHandler &handler,
163
			     void *handler_ctx)
164
{
165
	if (frame->nfields != 4)
166 167 168
		return;

	/* for now I only read the 4th field, with the fullstring */
169
	const id3_field *field = id3_frame_field(frame, 3);
Max Kellermann's avatar
Max Kellermann committed
170
	if (field == nullptr)
171 172
		return;

173
	const id3_ucs4_t *ucs4 = id3_field_getfullstring(field);
Max Kellermann's avatar
Max Kellermann committed
174
	if (ucs4 == nullptr)
175 176
		return;

177
	id3_utf8_t *utf8 = import_id3_string(ucs4);
Max Kellermann's avatar
Max Kellermann committed
178
	if (utf8 == nullptr)
179 180
		return;

181 182
	AtScopeExit(utf8) { free(utf8); };

183
	tag_handler_invoke_tag(handler, handler_ctx, type, (const char *)utf8);
184 185
}

186 187 188 189 190
/**
 * Import all comment frames (ID3v2.4.0 section 4.10).  This is a
 * wrapper for tag_id3_import_comment_frame().
 */
static void
191
tag_id3_import_comment(struct id3_tag *tag, const char *id, TagType type,
192
		       const TagHandler &handler, void *handler_ctx)
193 194 195
{
	const struct id3_frame *frame;
	for (unsigned i = 0;
Max Kellermann's avatar
Max Kellermann committed
196
	     (frame = id3_tag_findframe(tag, id, i)) != nullptr; ++i)
197
		tag_id3_import_comment_frame(frame, type,
198
					     handler, handler_ctx);
199 200
}

201
/**
202
 * Parse a TXXX name, and convert it to a TagType enum value.
203 204
 * Returns TAG_NUM_OF_ITEM_TYPES if the TXXX name is not understood.
 */
205
gcc_pure
206
static TagType
207
tag_id3_parse_txxx_name(const char *name) noexcept
208
{
209 210

	return tag_table_lookup(musicbrainz_txxx_tags, name);
211 212 213 214 215 216
}

/**
 * Import all known MusicBrainz tags from TXXX frames.
 */
static void
217
tag_id3_import_musicbrainz(struct id3_tag *id3_tag,
218
			   const TagHandler &handler,
219
			   void *handler_ctx)
220 221
{
	for (unsigned i = 0;; ++i) {
222
		const id3_frame *frame = id3_tag_findframe(id3_tag, "TXXX", i);
Max Kellermann's avatar
Max Kellermann committed
223
		if (frame == nullptr)
224 225
			break;

226
		id3_utf8_t *name = tag_id3_getstring(frame, 1);
Max Kellermann's avatar
Max Kellermann committed
227
		if (name == nullptr)
228 229
			continue;

230 231
		AtScopeExit(name) { free(name); };

232
		id3_utf8_t *value = tag_id3_getstring(frame, 2);
Max Kellermann's avatar
Max Kellermann committed
233
		if (value == nullptr)
234 235
			continue;

236 237
		AtScopeExit(value) { free(value); };

238 239 240 241
		tag_handler_invoke_pair(handler, handler_ctx,
					(const char *)name,
					(const char *)value);

242
		TagType type = tag_id3_parse_txxx_name((const char*)name);
243 244 245 246

		if (type != TAG_NUM_OF_ITEM_TYPES)
			tag_handler_invoke_tag(handler, handler_ctx,
					       type, (const char*)value);
247 248 249
	}
}

250 251 252 253
/**
 * Imports the MusicBrainz TrackId from the UFID tag.
 */
static void
254
tag_id3_import_ufid(struct id3_tag *id3_tag,
255
		    const TagHandler &handler, void *handler_ctx)
256 257
{
	for (unsigned i = 0;; ++i) {
258
		const id3_frame *frame = id3_tag_findframe(id3_tag, "UFID", i);
Max Kellermann's avatar
Max Kellermann committed
259
		if (frame == nullptr)
260 261
			break;

262
		id3_field *field = id3_frame_field(frame, 0);
Max Kellermann's avatar
Max Kellermann committed
263
		if (field == nullptr)
264 265
			continue;

266
		const id3_latin1_t *name = id3_field_getlatin1(field);
Max Kellermann's avatar
Max Kellermann committed
267
		if (name == nullptr ||
268 269 270 271
		    strcmp((const char *)name, "http://musicbrainz.org") != 0)
			continue;

		field = id3_frame_field(frame, 1);
Max Kellermann's avatar
Max Kellermann committed
272
		if (field == nullptr)
273 274
			continue;

275 276 277
		id3_length_t length;
		const id3_byte_t *value =
			id3_field_getbinarydata(field, &length);
Max Kellermann's avatar
Max Kellermann committed
278
		if (value == nullptr || length == 0)
279 280
			continue;

281
		std::string p((const char *)value, length);
282
		tag_handler_invoke_tag(handler, handler_ctx,
283
				       TAG_MUSICBRAINZ_TRACKID, p.c_str());
284 285 286
	}
}

287
void
288
scan_id3_tag(struct id3_tag *tag,
289
	     const TagHandler &handler, void *handler_ctx)
290 291 292 293 294 295 296
{
	tag_id3_import_text(tag, ID3_FRAME_ARTIST, TAG_ARTIST,
			    handler, handler_ctx);
	tag_id3_import_text(tag, ID3_FRAME_ALBUM_ARTIST,
			    TAG_ALBUM_ARTIST, handler, handler_ctx);
	tag_id3_import_text(tag, ID3_FRAME_ARTIST_SORT,
			    TAG_ARTIST_SORT, handler, handler_ctx);
297 298 299

	tag_id3_import_text(tag, "TSOA", TAG_ALBUM_SORT, handler, handler_ctx);

300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
	tag_id3_import_text(tag, ID3_FRAME_ALBUM_ARTIST_SORT,
			    TAG_ALBUM_ARTIST_SORT, handler, handler_ctx);
	tag_id3_import_text(tag, ID3_FRAME_TITLE, TAG_TITLE,
			    handler, handler_ctx);
	tag_id3_import_text(tag, ID3_FRAME_ALBUM, TAG_ALBUM,
			    handler, handler_ctx);
	tag_id3_import_text(tag, ID3_FRAME_TRACK, TAG_TRACK,
			    handler, handler_ctx);
	tag_id3_import_text(tag, ID3_FRAME_YEAR, TAG_DATE,
			    handler, handler_ctx);
	tag_id3_import_text(tag, ID3_FRAME_GENRE, TAG_GENRE,
			    handler, handler_ctx);
	tag_id3_import_text(tag, ID3_FRAME_COMPOSER, TAG_COMPOSER,
			    handler, handler_ctx);
	tag_id3_import_text(tag, "TPE3", TAG_PERFORMER,
			    handler, handler_ctx);
	tag_id3_import_text(tag, "TPE4", TAG_PERFORMER, handler, handler_ctx);
	tag_id3_import_comment(tag, ID3_FRAME_COMMENT, TAG_COMMENT,
			       handler, handler_ctx);
	tag_id3_import_text(tag, ID3_FRAME_DISC, TAG_DISC,
			    handler, handler_ctx);

	tag_id3_import_musicbrainz(tag, handler, handler_ctx);
	tag_id3_import_ufid(tag, handler, handler_ctx);
}

Max Kellermann's avatar
Max Kellermann committed
326 327
Tag *
tag_id3_import(struct id3_tag *tag)
328
{
329
	TagBuilder tag_builder;
330
	scan_id3_tag(tag, add_tag_handler, &tag_builder);
331 332
	return tag_builder.IsEmpty()
		? nullptr
333
		: tag_builder.CommitNew();
334 335
}

336 337 338 339 340 341 342 343
bool
tag_id3_scan(InputStream &is,
	     const TagHandler &handler, void *handler_ctx)
{
	UniqueId3Tag tag;

	try {
		tag = tag_id3_load(is);
344 345
		if (!tag)
			return false;
346 347 348 349 350 351 352 353
	} catch (const std::runtime_error &e) {
		LogError(e);
		return false;
	}

	scan_id3_tag(tag.get(), handler, handler_ctx);
	return true;
}