Id3Scan.cxx 8.7 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
void
290
scan_id3_tag(const struct id3_tag *tag, TagHandler &handler) noexcept
291 292
{
	tag_id3_import_text(tag, ID3_FRAME_ARTIST, TAG_ARTIST,
293
			    handler);
294
	tag_id3_import_text(tag, ID3_FRAME_ALBUM_ARTIST,
295
			    TAG_ALBUM_ARTIST, handler);
296
	tag_id3_import_text(tag, ID3_FRAME_ARTIST_SORT,
297
			    TAG_ARTIST_SORT, handler);
298

299
	tag_id3_import_text(tag, "TSOA", TAG_ALBUM_SORT, handler);
300

301
	tag_id3_import_text(tag, ID3_FRAME_ALBUM_ARTIST_SORT,
302
			    TAG_ALBUM_ARTIST_SORT, handler);
303
	tag_id3_import_text(tag, ID3_FRAME_TITLE, TAG_TITLE,
304
			    handler);
305
	tag_id3_import_text(tag, ID3_FRAME_ALBUM, TAG_ALBUM,
306
			    handler);
307
	tag_id3_import_text(tag, ID3_FRAME_TRACK, TAG_TRACK,
308
			    handler);
309
	tag_id3_import_text(tag, ID3_FRAME_YEAR, TAG_DATE,
310
			    handler);
311
	tag_id3_import_text(tag, ID3_FRAME_ORIGINAL_RELEASE_DATE, TAG_ORIGINAL_DATE,
312
			    handler);
313
	tag_id3_import_text(tag, ID3_FRAME_GENRE, TAG_GENRE,
314
			    handler);
315
	tag_id3_import_text(tag, ID3_FRAME_COMPOSER, TAG_COMPOSER,
316
			    handler);
317
	tag_id3_import_text(tag, "TPE3", TAG_PERFORMER,
318 319
			    handler);
	tag_id3_import_text(tag, "TPE4", TAG_PERFORMER, handler);
320
	tag_id3_import_text(tag, "TIT1", TAG_GROUPING, handler);
321
	tag_id3_import_comment(tag, ID3_FRAME_COMMENT, TAG_COMMENT,
322
			       handler);
323
	tag_id3_import_text(tag, ID3_FRAME_DISC, TAG_DISC,
324
			    handler);
skidoo23's avatar
skidoo23 committed
325 326
	tag_id3_import_text(tag, ID3_FRAME_LABEL, TAG_LABEL,
			    handler);
327

328 329
	tag_id3_import_musicbrainz(tag, handler);
	tag_id3_import_ufid(tag, handler);
330 331
}

332
Tag
333
tag_id3_import(const struct id3_tag *tag) noexcept
334
{
335
	TagBuilder tag_builder;
336 337
	AddTagHandler h(tag_builder);
	scan_id3_tag(tag, h);
338
	return tag_builder.Commit();
339 340
}

341
bool
342
tag_id3_scan(InputStream &is, TagHandler &handler) noexcept
343 344 345 346 347
{
	UniqueId3Tag tag;

	try {
		tag = tag_id3_load(is);
348 349
		if (!tag)
			return false;
350 351
	} catch (...) {
		LogError(std::current_exception());
352 353 354
		return false;
	}

355
	scan_id3_tag(tag.get(), handler);
356 357
	return true;
}