Id3Scan.cxx 9.32 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"
21
#include "Id3Scan.hxx"
22
#include "Id3Load.hxx"
23 24 25
#include "Handler.hxx"
#include "Table.hxx"
#include "Builder.hxx"
26
#include "util/Alloc.hxx"
27
#include "util/ScopeExit.hxx"
28
#include "util/StringUtil.hxx"
29
#include "Log.hxx"
Max Kellermann's avatar
Max Kellermann committed
30

31 32
#include <id3tag.h>

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

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

39 40 41 42 43 44 45
#  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
46 47 48 49
#ifndef ID3_FRAME_ARTIST_SORT
#define ID3_FRAME_ARTIST_SORT "TSOP"
#endif

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

#ifndef ID3_FRAME_ALBUM_ARTIST
#define ID3_FRAME_ALBUM_ARTIST "TPE2"
#endif

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

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

	return id3_ucs4_utf8duplicate(ucs4);
}

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

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

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

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

	/* check the encoding field */

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

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

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

	/* Get the number of strings available */
115 116 117
	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
118
		if (ucs4 == nullptr)
119 120
			continue;

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

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

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

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

135 136 137 138 139
/**
 * 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
140
tag_id3_import_text(struct id3_tag *tag, const char *id, TagType type,
141
		    const TagHandler &handler, void *handler_ctx)
142 143 144
{
	const struct id3_frame *frame;
	for (unsigned i = 0;
Max Kellermann's avatar
Max Kellermann committed
145
	     (frame = id3_tag_findframe(tag, id, i)) != nullptr; ++i)
146
		tag_id3_import_text_frame(frame, type,
147
					  handler, handler_ctx);
148 149
}

150 151 152 153 154 155 156 157 158 159
/**
 * 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
160
tag_id3_import_comment_frame(const struct id3_frame *frame, TagType type,
161
			     const TagHandler &handler,
162
			     void *handler_ctx)
163
{
164
	if (frame->nfields != 4)
165 166 167
		return;

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

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

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

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

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

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

200
/**
201
 * Parse a TXXX name, and convert it to a TagType enum value.
202 203
 * Returns TAG_NUM_OF_ITEM_TYPES if the TXXX name is not understood.
 */
204
gcc_pure
205
static TagType
206 207
tag_id3_parse_txxx_name(const char *name)
{
208
	static constexpr struct tag_table txxx_tags[] = {
209 210 211 212 213 214
		{ "ALBUMARTISTSORT", TAG_ALBUM_ARTIST_SORT },
		{ "MusicBrainz Artist Id", TAG_MUSICBRAINZ_ARTISTID },
		{ "MusicBrainz Album Id", TAG_MUSICBRAINZ_ALBUMID },
		{ "MusicBrainz Album Artist Id",
		  TAG_MUSICBRAINZ_ALBUMARTISTID },
		{ "MusicBrainz Track Id", TAG_MUSICBRAINZ_TRACKID },
215 216
		{ "MusicBrainz Release Track Id",
		  TAG_MUSICBRAINZ_RELEASETRACKID },
Max Kellermann's avatar
Max Kellermann committed
217
		{ nullptr, TAG_NUM_OF_ITEM_TYPES }
218 219
	};

220
	return tag_table_lookup(txxx_tags, name);
221 222 223 224 225 226
}

/**
 * Import all known MusicBrainz tags from TXXX frames.
 */
static void
227
tag_id3_import_musicbrainz(struct id3_tag *id3_tag,
228
			   const TagHandler &handler,
229
			   void *handler_ctx)
230 231
{
	for (unsigned i = 0;; ++i) {
232
		const id3_frame *frame = id3_tag_findframe(id3_tag, "TXXX", i);
Max Kellermann's avatar
Max Kellermann committed
233
		if (frame == nullptr)
234 235
			break;

236
		id3_utf8_t *name = tag_id3_getstring(frame, 1);
Max Kellermann's avatar
Max Kellermann committed
237
		if (name == nullptr)
238 239
			continue;

240 241
		AtScopeExit(name) { free(name); };

242
		id3_utf8_t *value = tag_id3_getstring(frame, 2);
Max Kellermann's avatar
Max Kellermann committed
243
		if (value == nullptr)
244 245
			continue;

246 247
		AtScopeExit(value) { free(value); };

248 249 250 251
		tag_handler_invoke_pair(handler, handler_ctx,
					(const char *)name,
					(const char *)value);

252
		TagType type = tag_id3_parse_txxx_name((const char*)name);
253 254 255 256

		if (type != TAG_NUM_OF_ITEM_TYPES)
			tag_handler_invoke_tag(handler, handler_ctx,
					       type, (const char*)value);
257 258 259
	}
}

260 261 262 263
/**
 * Imports the MusicBrainz TrackId from the UFID tag.
 */
static void
264
tag_id3_import_ufid(struct id3_tag *id3_tag,
265
		    const TagHandler &handler, void *handler_ctx)
266 267
{
	for (unsigned i = 0;; ++i) {
268
		const id3_frame *frame = id3_tag_findframe(id3_tag, "UFID", i);
Max Kellermann's avatar
Max Kellermann committed
269
		if (frame == nullptr)
270 271
			break;

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

276
		const id3_latin1_t *name = id3_field_getlatin1(field);
Max Kellermann's avatar
Max Kellermann committed
277
		if (name == nullptr ||
278 279 280 281
		    strcmp((const char *)name, "http://musicbrainz.org") != 0)
			continue;

		field = id3_frame_field(frame, 1);
Max Kellermann's avatar
Max Kellermann committed
282
		if (field == nullptr)
283 284
			continue;

285 286 287
		id3_length_t length;
		const id3_byte_t *value =
			id3_field_getbinarydata(field, &length);
Max Kellermann's avatar
Max Kellermann committed
288
		if (value == nullptr || length == 0)
289 290
			continue;

291
		std::string p((const char *)value, length);
292
		tag_handler_invoke_tag(handler, handler_ctx,
293
				       TAG_MUSICBRAINZ_TRACKID, p.c_str());
294 295 296
	}
}

297
void
298
scan_id3_tag(struct id3_tag *tag,
299
	     const TagHandler &handler, void *handler_ctx)
300 301 302 303 304 305 306
{
	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);
307 308 309

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

310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
	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
336 337
Tag *
tag_id3_import(struct id3_tag *tag)
338
{
339
	TagBuilder tag_builder;
340
	scan_id3_tag(tag, add_tag_handler, &tag_builder);
341 342
	return tag_builder.IsEmpty()
		? nullptr
343
		: tag_builder.CommitNew();
344 345
}

346 347 348 349 350 351 352 353
bool
tag_id3_scan(InputStream &is,
	     const TagHandler &handler, void *handler_ctx)
{
	UniqueId3Tag tag;

	try {
		tag = tag_id3_load(is);
354 355
		if (!tag)
			return false;
356 357 358 359 360 361 362 363
	} catch (const std::runtime_error &e) {
		LogError(e);
		return false;
	}

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