Id3Scan.cxx 9.55 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2020 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"
Max Kellermann's avatar
Max Kellermann committed
31

32 33
#include <id3tag.h>

34
#include <string.h>
35
#include <stdlib.h>
36

skidoo23's avatar
skidoo23 committed
37 38 39 40 41 42 43
#ifndef ID3_FRAME_COMPOSER
#define ID3_FRAME_COMPOSER "TCOM"
#endif

#ifndef ID3_FRAME_DISC
#define ID3_FRAME_DISC "TPOS"
#endif
44

Bart Nagel's avatar
Bart Nagel committed
45 46 47 48
#ifndef ID3_FRAME_ARTIST_SORT
#define ID3_FRAME_ARTIST_SORT "TSOP"
#endif

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

#ifndef ID3_FRAME_ALBUM_ARTIST
#define ID3_FRAME_ALBUM_ARTIST "TPE2"
#endif

57 58 59 60
#ifndef ID3_FRAME_ORIGINAL_RELEASE_DATE
#define ID3_FRAME_ORIGINAL_RELEASE_DATE "TDOR"
#endif

skidoo23's avatar
skidoo23 committed
61 62 63 64
#ifndef ID3_FRAME_LABEL
#define ID3_FRAME_LABEL "TPUB"
#endif

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

73
	const id3_ucs4_t *ucs4 = id3_field_getstring(field);
Max Kellermann's avatar
Max Kellermann committed
74 75
	if (ucs4 == nullptr)
		return nullptr;
76 77 78 79

	return id3_ucs4_utf8duplicate(ucs4);
}

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

89
	AtScopeExit(utf8) { free(utf8); };
90

91
	return (id3_utf8_t *)xstrdup(Strip((char *)utf8));
92 93
}

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

	/* check the encoding field */

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

115 116 117
	/* process the value(s) */

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

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

128
		if (type == TAG_GENRE)
129 130
			ucs4 = id3_genre_name(ucs4);

131
		id3_utf8_t *utf8 = import_id3_string(ucs4);
Max Kellermann's avatar
Max Kellermann committed
132
		if (utf8 == nullptr)
133 134
			continue;

135 136
		AtScopeExit(utf8) { free(utf8); };

137
		handler.OnTag(type, (const char *)utf8);
138
	}
139 140
}

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

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

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

177
	const id3_ucs4_t *ucs4 = id3_field_getfullstring(field);
Max Kellermann's avatar
Max Kellermann committed
178
	if (ucs4 == nullptr)
179 180
		return;

181
	id3_utf8_t *utf8 = import_id3_string(ucs4);
Max Kellermann's avatar
Max Kellermann committed
182
	if (utf8 == nullptr)
183 184
		return;

185 186
	AtScopeExit(utf8) { free(utf8); };

187
	handler.OnTag(type, (const char *)utf8);
188 189
}

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

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

	return tag_table_lookup(musicbrainz_txxx_tags, name);
215 216 217 218 219 220
}

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

229
		id3_utf8_t *name = tag_id3_getstring(frame, 1);
Max Kellermann's avatar
Max Kellermann committed
230
		if (name == nullptr)
231 232
			continue;

233 234
		AtScopeExit(name) { free(name); };

235
		id3_utf8_t *value = tag_id3_getstring(frame, 2);
Max Kellermann's avatar
Max Kellermann committed
236
		if (value == nullptr)
237 238
			continue;

239 240
		AtScopeExit(value) { free(value); };

241
		handler.OnPair((const char *)name, (const char *)value);
242

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

		if (type != TAG_NUM_OF_ITEM_TYPES)
246
			handler.OnTag(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(const struct id3_tag *id3_tag,
255
		    TagHandler &handler) noexcept
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 282
		handler.OnTag(TAG_MUSICBRAINZ_TRACKID,
			      {(const char *)value, length});
283 284 285
	}
}

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
/**
 * 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});
	}
}

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

337
	tag_id3_import_text(tag, "TSOA", TAG_ALBUM_SORT, handler);
338

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

366 367
	tag_id3_import_musicbrainz(tag, handler);
	tag_id3_import_ufid(tag, handler);
368
	tag_id3_handle_apic(tag, handler);
369 370
}

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

380
bool
381
tag_id3_scan(InputStream &is, TagHandler &handler)
382
{
383 384
	auto tag = tag_id3_load(is);
	if (!tag)
385 386
		return false;

387
	scan_id3_tag(tag.get(), handler);
388 389
	return true;
}