--- /dev/null
+/* GStreamer
+ * Copyright (C) 2003 Benjamin Otte <in7y118@public.uni-hamburg.de>
+ *
+ * gstid3tag.c: plugin for reading / modifying id3 tags
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+/**
+ * SECTION:gsttagid3
+ * @short_description: tag mappings and support functions for plugins
+ * dealing with ID3v1 and ID3v2 tags
+ * @see_also: #GstTagList
+ *
+ * <refsect2>
+ * <para>
+ * Contains various utility functions for plugins to parse or create
+ * ID3 tags and map ID3v2 identifiers to and from GStreamer identifiers.
+ * </para>
+ * </refsect2>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gsttageditingprivate.h"
+#include <stdlib.h>
+#include <string.h>
+
+static const gchar *genres[] = {
+ "Blues",
+ "Classic Rock",
+ "Country",
+ "Dance",
+ "Disco",
+ "Funk",
+ "Grunge",
+ "Hip-Hop",
+ "Jazz",
+ "Metal",
+ "New Age",
+ "Oldies",
+ "Other",
+ "Pop",
+ "R&B",
+ "Rap",
+ "Reggae",
+ "Rock",
+ "Techno",
+ "Industrial",
+ "Alternative",
+ "Ska",
+ "Death Metal",
+ "Pranks",
+ "Soundtrack",
+ "Euro-Techno",
+ "Ambient",
+ "Trip-Hop",
+ "Vocal",
+ "Jazz+Funk",
+ "Fusion",
+ "Trance",
+ "Classical",
+ "Instrumental",
+ "Acid",
+ "House",
+ "Game",
+ "Sound Clip",
+ "Gospel",
+ "Noise",
+ "Alternative Rock",
+ "Bass",
+ "Soul",
+ "Punk",
+ "Space",
+ "Meditative",
+ "Instrumental Pop",
+ "Instrumental Rock",
+ "Ethnic",
+ "Gothic",
+ "Darkwave",
+ "Techno-Industrial",
+ "Electronic",
+ "Pop-Folk",
+ "Eurodance",
+ "Dream",
+ "Southern Rock",
+ "Comedy",
+ "Cult",
+ "Gangsta",
+ "Top 40",
+ "Christian Rap",
+ "Pop/Funk",
+ "Jungle",
+ "Native American",
+ "Cabaret",
+ "New Wave",
+ "Psychedelic",
+ "Rave",
+ "Showtunes",
+ "Trailer",
+ "Lo-Fi",
+ "Tribal",
+ "Acid Punk",
+ "Acid Jazz",
+ "Polka",
+ "Retro",
+ "Musical",
+ "Rock & Roll",
+ "Hard Rock",
+ "Folk",
+ "Folk/Rock",
+ "National Folk",
+ "Swing",
+ "Fusion",
+ "Bebob",
+ "Latin",
+ "Revival",
+ "Celtic",
+ "Bluegrass",
+ "Avantgarde",
+ "Gothic Rock",
+ "Progressive Rock",
+ "Psychedelic Rock",
+ "Symphonic Rock",
+ "Slow Rock",
+ "Big Band",
+ "Chorus",
+ "Easy Listening",
+ "Acoustic",
+ "Humour",
+ "Speech",
+ "Chanson",
+ "Opera",
+ "Chamber Music",
+ "Sonata",
+ "Symphony",
+ "Booty Bass",
+ "Primus",
+ "Porn Groove",
+ "Satire",
+ "Slow Jam",
+ "Club",
+ "Tango",
+ "Samba",
+ "Folklore",
+ "Ballad",
+ "Power Ballad",
+ "Rhythmic Soul",
+ "Freestyle",
+ "Duet",
+ "Punk Rock",
+ "Drum Solo",
+ "A Capella",
+ "Euro-House",
+ "Dance Hall",
+ "Goa",
+ "Drum & Bass",
+ "Club-House",
+ "Hardcore",
+ "Terror",
+ "Indie",
+ "BritPop",
+ "Negerpunk",
+ "Polsk Punk",
+ "Beat",
+ "Christian Gangsta Rap",
+ "Heavy Metal",
+ "Black Metal",
+ "Crossover",
+ "Contemporary Christian",
+ "Christian Rock",
+ "Merengue",
+ "Salsa",
+ "Thrash Metal",
+ "Anime",
+ "Jpop",
+ "Synthpop"
+};
+
+static const GstTagEntryMatch tag_matches[] = {
+ {GST_TAG_TITLE, "TIT2"},
+ {GST_TAG_ALBUM, "TALB"},
+ {GST_TAG_TRACK_NUMBER, "TRCK"},
+ {GST_TAG_ARTIST, "TPE1"},
+ {GST_TAG_ALBUM_ARTIST, "TPE2"},
+ {GST_TAG_COMPOSER, "TCOM"},
+ {GST_TAG_COPYRIGHT, "TCOP"},
+ {GST_TAG_COPYRIGHT_URI, "WCOP"},
+ {GST_TAG_ENCODED_BY, "TENC"},
+ {GST_TAG_GENRE, "TCON"},
+ {GST_TAG_DATE, "TDRC"},
+ {GST_TAG_COMMENT, "COMM"},
+ {GST_TAG_ALBUM_VOLUME_NUMBER, "TPOS"},
+ {GST_TAG_DURATION, "TLEN"},
+ {GST_TAG_ISRC, "TSRC"},
+ {GST_TAG_IMAGE, "APIC"},
+ {GST_TAG_ENCODER, "TSSE"},
+ {GST_TAG_BEATS_PER_MINUTE, "TBPM"},
+ {GST_TAG_ARTIST_SORTNAME, "TSOP"},
+ {GST_TAG_ALBUM_SORTNAME, "TSOA"},
+ {GST_TAG_TITLE_SORTNAME, "TSOT"},
+ {NULL, NULL}
+};
+
+/**
+ * gst_tag_from_id3_tag:
+ * @id3_tag: ID3v2 tag to convert to GStreamer tag
+ *
+ * Looks up the GStreamer tag for a ID3v2 tag.
+ *
+ * Returns: The corresponding GStreamer tag or NULL if none exists.
+ */
+G_CONST_RETURN gchar *
+gst_tag_from_id3_tag (const gchar * id3_tag)
+{
+ int i = 0;
+
+ g_return_val_if_fail (id3_tag != NULL, NULL);
+
+ while (tag_matches[i].gstreamer_tag != NULL) {
+ if (strncmp (id3_tag, tag_matches[i].original_tag, 5) == 0) {
+ return tag_matches[i].gstreamer_tag;
+ }
+ i++;
+ }
+
+ GST_INFO ("Cannot map ID3v2 tag '%c%c%c%c' to GStreamer tag",
+ id3_tag[0], id3_tag[1], id3_tag[2], id3_tag[3]);
+
+ return NULL;
+}
+
+static const GstTagEntryMatch user_tag_matches[] = {
+ /* musicbrainz identifiers being used in the real world (foobar2000) */
+ {GST_TAG_MUSICBRAINZ_ARTISTID, "TXXX|musicbrainz_artistid"},
+ {GST_TAG_MUSICBRAINZ_ALBUMID, "TXXX|musicbrainz_albumid"},
+ {GST_TAG_MUSICBRAINZ_ALBUMARTISTID, "TXXX|musicbrainz_albumartistid"},
+ {GST_TAG_MUSICBRAINZ_TRMID, "TXXX|musicbrainz_trmid"},
+ {GST_TAG_CDDA_MUSICBRAINZ_DISCID, "TXXX|musicbrainz_discid"},
+ /* musicbrainz identifiers according to spec no one pays
+ * attention to (http://musicbrainz.org/docs/specs/metadata_tags.html) */
+ {GST_TAG_MUSICBRAINZ_ARTISTID, "TXXX|MusicBrainz Artist Id"},
+ {GST_TAG_MUSICBRAINZ_ALBUMID, "TXXX|MusicBrainz Album Id"},
+ {GST_TAG_MUSICBRAINZ_ALBUMARTISTID, "TXXX|MusicBrainz Album Artist Id"},
+ {GST_TAG_MUSICBRAINZ_TRMID, "TXXX|MusicBrainz TRM Id"},
+ /* according to: http://wiki.musicbrainz.org/MusicBrainzTag (yes, no space
+ * before 'ID' and not 'Id' either this time, yay for consistency) */
+ {GST_TAG_CDDA_MUSICBRAINZ_DISCID, "TXXX|MusicBrainz DiscID"},
+ /* foobar2000 uses these identifiers to store gain/peak information in
+ * ID3v2 tags <= v2.3.0. In v2.4.0 there's the RVA2 frame for that */
+ {GST_TAG_TRACK_GAIN, "TXXX|replaygain_track_gain"},
+ {GST_TAG_TRACK_PEAK, "TXXX|replaygain_track_peak"},
+ {GST_TAG_ALBUM_GAIN, "TXXX|replaygain_album_gain"},
+ {GST_TAG_ALBUM_PEAK, "TXXX|replaygain_album_peak"},
+ /* the following two are more or less made up, there seems to be little
+ * evidence that any popular application is actually putting this info
+ * into TXXX frames; the first one comes from a musicbrainz wiki 'proposed
+ * tags' page, the second one is analogue to the vorbis/ape/flac tag. */
+ {GST_TAG_CDDA_CDDB_DISCID, "TXXX|discid"},
+ {GST_TAG_CDDA_CDDB_DISCID, "TXXX|CDDB DiscID"}
+};
+
+/**
+ * gst_tag_from_id3_user_tag:
+ * @type: the type of ID3v2 user tag (e.g. "TXXX" or "UDIF")
+ * @id3_user_tag: ID3v2 user tag to convert to GStreamer tag
+ *
+ * Looks up the GStreamer tag for an ID3v2 user tag (e.g. description in
+ * TXXX frame or owner in UFID frame).
+ *
+ * Returns: The corresponding GStreamer tag or NULL if none exists.
+ */
+G_CONST_RETURN gchar *
+gst_tag_from_id3_user_tag (const gchar * type, const gchar * id3_user_tag)
+{
+ int i = 0;
+
+ g_return_val_if_fail (type != NULL && strlen (type) == 4, NULL);
+ g_return_val_if_fail (id3_user_tag != NULL, NULL);
+
+ for (i = 0; i < G_N_ELEMENTS (user_tag_matches); ++i) {
+ if (strncmp (type, user_tag_matches[i].original_tag, 4) == 0 &&
+ g_ascii_strcasecmp (id3_user_tag,
+ user_tag_matches[i].original_tag + 5) == 0) {
+ GST_LOG ("Mapped ID3v2 user tag '%s' to GStreamer tag '%s'",
+ user_tag_matches[i].original_tag, user_tag_matches[i].gstreamer_tag);
+ return user_tag_matches[i].gstreamer_tag;
+ }
+ }
+
+ GST_INFO ("Cannot map ID3v2 user tag '%s' of type '%s' to GStreamer tag",
+ id3_user_tag, type);
+
+ return NULL;
+}
+
+/**
+ * gst_tag_to_id3_tag:
+ * @gst_tag: GStreamer tag to convert to vorbiscomment tag
+ *
+ * Looks up the ID3v2 tag for a GStreamer tag.
+ *
+ * Returns: The corresponding ID3v2 tag or NULL if none exists.
+ */
+G_CONST_RETURN gchar *
+gst_tag_to_id3_tag (const gchar * gst_tag)
+{
+ int i = 0;
+
+ g_return_val_if_fail (gst_tag != NULL, NULL);
+
+ while (tag_matches[i].gstreamer_tag != NULL) {
+ if (strcmp (gst_tag, tag_matches[i].gstreamer_tag) == 0) {
+ return tag_matches[i].original_tag;
+ }
+ i++;
+ }
+ return NULL;
+}
+
+static void
+gst_tag_extract_id3v1_string (GstTagList * list, const gchar * tag,
+ const gchar * start, const guint size)
+{
+ const gchar *env_vars[] = { "GST_ID3V1_TAG_ENCODING",
+ "GST_ID3_TAG_ENCODING", "GST_TAG_ENCODING", NULL
+ };
+ gchar *utf8;
+
+ utf8 = gst_tag_freeform_string_to_utf8 (start, size, env_vars);
+
+ if (utf8 && *utf8 != '\0') {
+ gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, tag, utf8, NULL);
+ }
+
+ g_free (utf8);
+}
+
+/**
+ * gst_tag_list_new_from_id3v1:
+ * @data: 128 bytes of data containing the ID3v1 tag
+ *
+ * Parses the data containing an ID3v1 tag and returns a #GstTagList from the
+ * parsed data.
+ *
+ * Returns: A new tag list or NULL if the data was not an ID3v1 tag.
+ */
+GstTagList *
+gst_tag_list_new_from_id3v1 (const guint8 * data)
+{
+ guint year;
+ gchar *ystr;
+ GstTagList *list;
+
+ g_return_val_if_fail (data != NULL, NULL);
+
+ if (data[0] != 'T' || data[1] != 'A' || data[2] != 'G')
+ return NULL;
+ list = gst_tag_list_new ();
+ gst_tag_extract_id3v1_string (list, GST_TAG_TITLE, (gchar *) & data[3], 30);
+ gst_tag_extract_id3v1_string (list, GST_TAG_ARTIST, (gchar *) & data[33], 30);
+ gst_tag_extract_id3v1_string (list, GST_TAG_ALBUM, (gchar *) & data[63], 30);
+ ystr = g_strndup ((gchar *) & data[93], 4);
+ year = strtoul (ystr, NULL, 10);
+ g_free (ystr);
+ if (year > 0) {
+ GDate *date = g_date_new_dmy (1, 1, year);
+
+ gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, GST_TAG_DATE, date, NULL);
+ g_date_free (date);
+ }
+ if (data[125] == 0 && data[126] != 0) {
+ gst_tag_extract_id3v1_string (list, GST_TAG_COMMENT, (gchar *) & data[97],
+ 28);
+ gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, GST_TAG_TRACK_NUMBER,
+ (guint) data[126], NULL);
+ } else {
+ gst_tag_extract_id3v1_string (list, GST_TAG_COMMENT, (gchar *) & data[97],
+ 30);
+ }
+ if (data[127] < gst_tag_id3_genre_count () && !gst_tag_list_is_empty (list)) {
+ gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, GST_TAG_GENRE,
+ gst_tag_id3_genre_get (data[127]), NULL);
+ }
+
+ return list;
+}
+
+/**
+ * gst_tag_id3_genre_count:
+ *
+ * Gets the number of ID3v1 genres that can be identified. Winamp genres are
+ * included.
+ *
+ * Returns: the number of ID3v1 genres that can be identified
+ */
+guint
+gst_tag_id3_genre_count (void)
+{
+ return G_N_ELEMENTS (genres);
+}
+
+/**
+ * gst_tag_id3_genre_get:
+ * @id: ID of genre to query
+ *
+ * Gets the ID3v1 genre name for a given ID.
+ *
+ * Returns: the genre or NULL if no genre is associated with that ID.
+ */
+G_CONST_RETURN gchar *
+gst_tag_id3_genre_get (const guint id)
+{
+ if (id >= G_N_ELEMENTS (genres))
+ return NULL;
+ return genres[id];
+}
+
+/**
+ * gst_tag_list_add_id3_image:
+ * @tag_list: a tag list
+ * @image_data: the (encoded) image
+ * @image_data_len: the length of the encoded image data at @image_data
+ * @id3_picture_type: picture type as per the ID3 (v2.4.0) specification for
+ * the APIC frame (0 = unknown/other)
+ *
+ * Adds an image from an ID3 APIC frame (or similar, such as used in FLAC)
+ * to the given tag list. Also see gst_tag_image_data_to_image_buffer() for
+ * more information on image tags in GStreamer.
+ *
+ * Returns: %TRUE if the image was processed, otherwise %FALSE
+ *
+ * Since: 0.10.20
+ */
+gboolean
+gst_tag_list_add_id3_image (GstTagList * tag_list, const guint8 * image_data,
+ guint image_data_len, guint id3_picture_type)
+{
+ GstTagImageType tag_image_type;
+ const gchar *tag_name;
+ GstBuffer *image;
+
+ g_return_val_if_fail (GST_IS_TAG_LIST (tag_list), FALSE);
+ g_return_val_if_fail (image_data != NULL, FALSE);
+ g_return_val_if_fail (image_data_len > 0, FALSE);
+
+ if (id3_picture_type == 0x01 || id3_picture_type == 0x02) {
+ /* file icon for preview. Don't add image-type to caps, since there
+ * is only supposed to be one of these, and the type is already indicated
+ * via the special tag */
+ tag_name = GST_TAG_PREVIEW_IMAGE;
+ tag_image_type = GST_TAG_IMAGE_TYPE_NONE;
+ } else {
+ tag_name = GST_TAG_IMAGE;
+
+ /* Remap the ID3v2 APIC type our ImageType enum */
+ if (id3_picture_type >= 0x3 && id3_picture_type <= 0x14)
+ tag_image_type = (GstTagImageType) (id3_picture_type - 2);
+ else
+ tag_image_type = GST_TAG_IMAGE_TYPE_UNDEFINED;
+ }
+
+ image = gst_tag_image_data_to_image_buffer (image_data, image_data_len,
+ tag_image_type);
+
+ if (image == NULL)
+ return FALSE;
+
+ gst_tag_list_add (tag_list, GST_TAG_MERGE_APPEND, tag_name, image, NULL);
+ gst_buffer_unref (image);
+ return TRUE;
+}