Id3Scan.cxx 9.69 KB
Newer Older
1
/*
2
 * Copyright 2003-2019 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 "Id3Scan.hxx"
21
#include "Id3Load.hxx"
22 23 24
#include "Handler.hxx"
#include "Table.hxx"
#include "Builder.hxx"
25
#include "Tag.hxx"
26
#include "Id3MusicBrainz.hxx"
27
#include "util/Alloc.hxx"
28
#include "util/ScopeExit.hxx"
29
#include "util/StringStrip.hxx"
30
#include "util/StringView.hxx"
31
#include "Log.hxx"
Max Kellermann's avatar
Max Kellermann committed
32

33 34
#include <id3tag.h>

35
#include <exception>
36

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

skidoo23's avatar
skidoo23 committed
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
47

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

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

#ifndef ID3_FRAME_ALBUM_ARTIST
#define ID3_FRAME_ALBUM_ARTIST "TPE2"
#endif

60 61 62 63
#ifndef ID3_FRAME_ORIGINAL_RELEASE_DATE
#define ID3_FRAME_ORIGINAL_RELEASE_DATE "TDOR"
#endif

skidoo23's avatar
skidoo23 committed
64 65 66 67
#ifndef ID3_FRAME_LABEL
#define ID3_FRAME_LABEL "TPUB"
#endif

68
gcc_pure
69
static id3_utf8_t *
70
tag_id3_getstring(const struct id3_frame *frame, unsigned i) noexcept
71
{
72
	id3_field *field = id3_frame_field(frame, i);
Max Kellermann's avatar
Max Kellermann committed
73 74
	if (field == nullptr)
		return nullptr;
75

76
	const id3_ucs4_t *ucs4 = id3_field_getstring(field);
Max Kellermann's avatar
Max Kellermann committed
77 78
	if (ucs4 == nullptr)
		return nullptr;
79 80 81 82

	return id3_ucs4_utf8duplicate(ucs4);
}

83 84
/* This will try to convert a string to utf-8,
 */
Max Kellermann's avatar
Max Kellermann committed
85
static id3_utf8_t *
86
import_id3_string(const id3_ucs4_t *ucs4)
87
{
88 89 90
	id3_utf8_t *utf8 = id3_ucs4_utf8duplicate(ucs4);
	if (gcc_unlikely(utf8 == nullptr))
		return nullptr;
91

92
	AtScopeExit(utf8) { free(utf8); };
93

94
	return (id3_utf8_t *)xstrdup(Strip((char *)utf8));
95 96
}

97 98 99 100 101 102 103
/**
 * Import a "Text information frame" (ID3v2.4.0 section 4.2).  It
 * contains 2 fields:
 *
 * - encoding
 * - string list
 */
104
static void
105
tag_id3_import_text_frame(const struct id3_frame *frame,
106
			  TagType type,
107
			  TagHandler &handler) noexcept
108
{
109
	if (frame->nfields != 2)
110
		return;
111 112 113

	/* check the encoding field */

114
	const id3_field *field = id3_frame_field(frame, 0);
Max Kellermann's avatar
Max Kellermann committed
115
	if (field == nullptr || field->type != ID3_FIELD_TYPE_TEXTENCODING)
116
		return;
117

118 119 120
	/* process the value(s) */

	field = id3_frame_field(frame, 1);
Max Kellermann's avatar
Max Kellermann committed
121
	if (field == nullptr || field->type != ID3_FIELD_TYPE_STRINGLIST)
122 123 124
		return;

	/* Get the number of strings available */
125 126 127
	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
128
		if (ucs4 == nullptr)
129 130
			continue;

131
		if (type == TAG_GENRE)
132 133
			ucs4 = id3_genre_name(ucs4);

134
		id3_utf8_t *utf8 = import_id3_string(ucs4);
Max Kellermann's avatar
Max Kellermann committed
135
		if (utf8 == nullptr)
136 137
			continue;

138 139
		AtScopeExit(utf8) { free(utf8); };

140
		handler.OnTag(type, (const char *)utf8);
141
	}
142 143
}

144 145 146 147 148
/**
 * 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
149
tag_id3_import_text(const struct id3_tag *tag, const char *id, TagType type,
150
		    TagHandler &handler) noexcept
151 152 153
{
	const struct id3_frame *frame;
	for (unsigned i = 0;
Max Kellermann's avatar
Max Kellermann committed
154
	     (frame = id3_tag_findframe(tag, id, i)) != nullptr; ++i)
155
		tag_id3_import_text_frame(frame, type,
156
					  handler);
157 158
}

159 160 161 162 163 164 165 166 167 168
/**
 * 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
169
tag_id3_import_comment_frame(const struct id3_frame *frame, TagType type,
170
			     TagHandler &handler) noexcept
171
{
172
	if (frame->nfields != 4)
173 174 175
		return;

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

180
	const id3_ucs4_t *ucs4 = id3_field_getfullstring(field);
Max Kellermann's avatar
Max Kellermann committed
181
	if (ucs4 == nullptr)
182 183
		return;

184
	id3_utf8_t *utf8 = import_id3_string(ucs4);
Max Kellermann's avatar
Max Kellermann committed
185
	if (utf8 == nullptr)
186 187
		return;

188 189
	AtScopeExit(utf8) { free(utf8); };

190
	handler.OnTag(type, (const char *)utf8);
191 192
}

193 194 195 196 197
/**
 * Import all comment frames (ID3v2.4.0 section 4.10).  This is a
 * wrapper for tag_id3_import_comment_frame().
 */
static void
198
tag_id3_import_comment(const struct id3_tag *tag, const char *id, TagType type,
199
		       TagHandler &handler) noexcept
200 201 202
{
	const struct id3_frame *frame;
	for (unsigned i = 0;
Max Kellermann's avatar
Max Kellermann committed
203
	     (frame = id3_tag_findframe(tag, id, i)) != nullptr; ++i)
204
		tag_id3_import_comment_frame(frame, type,
205
					     handler);
206 207
}

208
/**
209
 * Parse a TXXX name, and convert it to a TagType enum value.
210 211
 * Returns TAG_NUM_OF_ITEM_TYPES if the TXXX name is not understood.
 */
212
gcc_pure
213
static TagType
214
tag_id3_parse_txxx_name(const char *name) noexcept
215
{
216 217

	return tag_table_lookup(musicbrainz_txxx_tags, name);
218 219 220 221 222 223
}

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

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

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

238
		id3_utf8_t *value = tag_id3_getstring(frame, 2);
Max Kellermann's avatar
Max Kellermann committed
239
		if (value == nullptr)
240 241
			continue;

242 243
		AtScopeExit(value) { free(value); };

244
		handler.OnPair((const char *)name, (const char *)value);
245

246
		TagType type = tag_id3_parse_txxx_name((const char*)name);
247 248

		if (type != TAG_NUM_OF_ITEM_TYPES)
249
			handler.OnTag(type, (const char*)value);
250 251 252
	}
}

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

265
		id3_field *field = id3_frame_field(frame, 0);
Max Kellermann's avatar
Max Kellermann committed
266
		if (field == nullptr)
267 268
			continue;

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

		field = id3_frame_field(frame, 1);
Max Kellermann's avatar
Max Kellermann committed
275
		if (field == nullptr)
276 277
			continue;

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

284 285
		handler.OnTag(TAG_MUSICBRAINZ_TRACKID,
			      {(const char *)value, length});
286 287 288
	}
}

289 290 291 292 293 294 295 296 297 298 299 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 326 327 328 329
/**
 * Handle "APIC" ("attached picture") tags.
 */
static void
tag_id3_handle_apic(const struct id3_tag *id3_tag,
		    TagHandler &handler) noexcept
{
	if (!handler.WantPicture())
		return;

	for (unsigned i = 0;; ++i) {
		const id3_frame *frame = id3_tag_findframe(id3_tag, "APIC", i);
		if (frame == nullptr)
			break;

		id3_field *mime_type_field = id3_frame_field(frame, 1);
		if (mime_type_field == nullptr)
			continue;

		const char *mime_type = (const char *)
			id3_field_getlatin1(mime_type_field);
		if (mime_type != nullptr &&
		    StringIsEqual(mime_type, "-->"))
			/* this is a URL, not image data */
			continue;

		id3_field *data_field = id3_frame_field(frame, 4);
		if (data_field == nullptr ||
		    data_field->type != ID3_FIELD_TYPE_BINARYDATA)
			continue;

		id3_length_t size;
		const id3_byte_t *data =
			id3_field_getbinarydata(data_field, &size);
		if (data == nullptr || size == 0)
			continue;

		handler.OnPicture(mime_type, {data, size});
	}
}

330
void
331
scan_id3_tag(const struct id3_tag *tag, TagHandler &handler) noexcept
332 333
{
	tag_id3_import_text(tag, ID3_FRAME_ARTIST, TAG_ARTIST,
334
			    handler);
335
	tag_id3_import_text(tag, ID3_FRAME_ALBUM_ARTIST,
336
			    TAG_ALBUM_ARTIST, handler);
337
	tag_id3_import_text(tag, ID3_FRAME_ARTIST_SORT,
338
			    TAG_ARTIST_SORT, handler);
339

340
	tag_id3_import_text(tag, "TSOA", TAG_ALBUM_SORT, handler);
341

342
	tag_id3_import_text(tag, ID3_FRAME_ALBUM_ARTIST_SORT,
343
			    TAG_ALBUM_ARTIST_SORT, handler);
344
	tag_id3_import_text(tag, ID3_FRAME_TITLE, TAG_TITLE,
345
			    handler);
346
	tag_id3_import_text(tag, ID3_FRAME_ALBUM, TAG_ALBUM,
347
			    handler);
348
	tag_id3_import_text(tag, ID3_FRAME_TRACK, TAG_TRACK,
349
			    handler);
350
	tag_id3_import_text(tag, ID3_FRAME_YEAR, TAG_DATE,
351
			    handler);
352
	tag_id3_import_text(tag, ID3_FRAME_ORIGINAL_RELEASE_DATE, TAG_ORIGINAL_DATE,
353
			    handler);
354
	tag_id3_import_text(tag, ID3_FRAME_GENRE, TAG_GENRE,
355
			    handler);
356
	tag_id3_import_text(tag, ID3_FRAME_COMPOSER, TAG_COMPOSER,
357
			    handler);
358
	tag_id3_import_text(tag, "TPE3", TAG_PERFORMER,
359 360
			    handler);
	tag_id3_import_text(tag, "TPE4", TAG_PERFORMER, handler);
361
	tag_id3_import_text(tag, "TIT1", TAG_GROUPING, handler);
362
	tag_id3_import_comment(tag, ID3_FRAME_COMMENT, TAG_COMMENT,
363
			       handler);
364
	tag_id3_import_text(tag, ID3_FRAME_DISC, TAG_DISC,
365
			    handler);
skidoo23's avatar
skidoo23 committed
366 367
	tag_id3_import_text(tag, ID3_FRAME_LABEL, TAG_LABEL,
			    handler);
368

369 370
	tag_id3_import_musicbrainz(tag, handler);
	tag_id3_import_ufid(tag, handler);
371
	tag_id3_handle_apic(tag, handler);
372 373
}

374
Tag
375
tag_id3_import(const struct id3_tag *tag) noexcept
376
{
377
	TagBuilder tag_builder;
378 379
	AddTagHandler h(tag_builder);
	scan_id3_tag(tag, h);
380
	return tag_builder.Commit();
381 382
}

383
bool
384
tag_id3_scan(InputStream &is, TagHandler &handler) noexcept
385 386 387 388 389
{
	UniqueId3Tag tag;

	try {
		tag = tag_id3_load(is);
390 391
		if (!tag)
			return false;
392 393
	} catch (...) {
		LogError(std::current_exception());
394 395 396
		return false;
	}

397
	scan_id3_tag(tag.get(), handler);
398 399
	return true;
}