/* * Copyright (C) 2005-2007 Christophe Fergeau * * * The code contained in this file is free software; you can redistribute * it and/or modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either version * 2.1 of the License, or (at your option) any later version. * * This file 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this code; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * iTunes and iPod are trademarks of Apple * * This product is not supported/written/published by Apple! * */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include "itdb.h" #include "itdb_private.h" #include "itdb_endianness.h" #include "db-artwork-debug.h" #include "db-artwork-parser.h" #include "db-image-parser.h" #include "db-itunes-parser.h" #include "db-parse-context.h" #include typedef int (*ParseListItem)(DBParseContext *ctx, GError *error); static Itdb_Track * get_song_by_dbid (Itdb_iTunesDB *db, guint64 id) { GList *it; for (it = db->tracks; it != NULL; it = it->next) { Itdb_Track *song; song = (Itdb_Track*)it->data; if (song->dbid == id) { return song; } } return NULL; } static int parse_mhif (DBParseContext *ctx, GError *error) { MhifHeader *mhif; mhif = db_parse_context_get_m_header (ctx, MhifHeader, "mhif"); if (mhif == NULL) { return -1; } dump_mhif (mhif); db_parse_context_set_total_len (ctx, get_gint32 (mhif->total_len, ctx->byte_order)); return 0; } static int parse_mhia (DBParseContext *ctx, Itdb_PhotoAlbum *photo_album, GError *error) { MhiaHeader *mhia; guint32 image_id; mhia = db_parse_context_get_m_header (ctx, MhiaHeader, "mhia"); if (mhia == NULL) { return -1; } dump_mhia (mhia); image_id = get_gint32 (mhia->image_id, ctx->byte_order); photo_album->members = g_list_append (photo_album->members, GUINT_TO_POINTER(image_id)); db_parse_context_set_total_len (ctx, get_gint32_db (ctx->db, mhia->total_len)); return 0; } static char * get_utf16_string (void* buffer, gint length, guint byte_order) { char *result; gunichar2 *tmp; int i; /* Byte-swap the utf16 characters if necessary (I'm relying * on gcc to optimize most of this code away on LE platforms) */ tmp = g_memdup (buffer, length); for (i = 0; i < length/2; i++) { tmp[i] = get_gint16 (tmp[i], byte_order); } result = g_utf16_to_utf8 (tmp, length/2, NULL, NULL, NULL); g_free (tmp); return result; } struct ParsedMhodString { enum MhodType mhod_type; char *mhod_string; }; static struct ParsedMhodString * parse_mhod_string (DBParseContext *ctx, GError *error) { struct ParsedMhodString *result; ArtworkDB_MhodHeaderString *mhod_string; ArtworkDB_MhodHeader *mhod; gint len; mhod = db_parse_context_get_m_header (ctx, ArtworkDB_MhodHeader, "mhod"); if (mhod == NULL) { return NULL; } db_parse_context_set_total_len (ctx, get_gint32 (mhod->total_len, ctx->byte_order)); if (get_gint32 (mhod->total_len, ctx->byte_order) < sizeof (ArtworkDB_MhodHeaderString)){ return NULL; } result = g_new0 (struct ParsedMhodString, 1); if (result == NULL) { return NULL; } mhod_string = (ArtworkDB_MhodHeaderString*)mhod; result->mhod_type = get_gint16 (mhod_string->type, ctx->byte_order); len = get_gint32 (mhod_string->string_len, ctx->byte_order); switch (mhod_string->encoding) { case 2: result->mhod_string = get_utf16_string ((gunichar2 *)mhod_string->string, len, ctx->byte_order); break; case 0: case 1: result->mhod_string = g_strndup (mhod_string->string, len); break; default: g_warning (_("Unexpected mhod string type: %d\n"), mhod_string->encoding); break; } dump_mhod_string (mhod_string); return result; } static int parse_mhod_3 (DBParseContext *ctx, Itdb_Thumb_Ipod_Item *thumb, GError *error) { struct ParsedMhodString *mhod; mhod = parse_mhod_string (ctx, error); if (mhod == NULL) { return -1; } if (mhod->mhod_type != MHOD_ARTWORK_TYPE_FILE_NAME) { g_free (mhod->mhod_string); g_free (mhod); return -1; } thumb->filename = mhod->mhod_string; g_free (mhod); return 0; } static int parse_photo_mhni (DBParseContext *ctx, Itdb_Thumb_Ipod *thumbs, GError *error) { MhniHeader *mhni; DBParseContext *mhod_ctx; Itdb_Thumb_Ipod_Item *thumb; mhni = db_parse_context_get_m_header (ctx, MhniHeader, "mhni"); if (mhni == NULL) { return -1; } db_parse_context_set_total_len (ctx, get_gint32 (mhni->total_len, ctx->byte_order)); dump_mhni (mhni); thumb = ipod_image_new_from_mhni (mhni, ctx->db); if (thumb == NULL) { return 0; } itdb_thumb_ipod_add (thumbs, thumb); mhod_ctx = db_parse_context_get_sub_context (ctx, ctx->header_len); if (mhod_ctx == NULL) { return -1; } parse_mhod_3 (mhod_ctx, thumb, error); g_free (mhod_ctx); return 0; } static int parse_photo_mhod (DBParseContext *ctx, Itdb_Thumb_Ipod *thumbs, GError *error) { ArtworkDB_MhodHeader *mhod; DBParseContext *mhni_ctx; gint32 type; mhod = db_parse_context_get_m_header (ctx, ArtworkDB_MhodHeader, "mhod"); if (mhod == NULL) { return -1; } db_parse_context_set_total_len (ctx, get_gint32 (mhod->total_len, ctx->byte_order)); type = get_gint16 (mhod->type, ctx->byte_order); dump_mhod (mhod); /* if this is a container... */ if (type == MHOD_ARTWORK_TYPE_THUMBNAIL) { mhni_ctx = db_parse_context_get_sub_context (ctx, ctx->header_len); if (mhni_ctx == NULL) { return -1; } parse_photo_mhni (mhni_ctx, thumbs, NULL); g_free (mhni_ctx); } return 0; } static int parse_mhii (DBParseContext *ctx, GError *error) { MhiiHeader *mhii; DBParseContext *mhod_ctx; int num_children; off_t cur_offset; Itdb_Artwork *artwork; Itdb_PhotoDB *photodb; guint64 mactime; Itdb_Device *device = db_get_device (ctx->db); Itdb_Thumb_Ipod *thumbs; mhii = db_parse_context_get_m_header (ctx, MhiiHeader, "mhii"); if (mhii == NULL) { return -1; } db_parse_context_set_total_len (ctx, get_gint32 (mhii->total_len, ctx->byte_order)); dump_mhii (mhii); artwork = itdb_artwork_new (); artwork->id = get_gint32 (mhii->image_id, ctx->byte_order); artwork->unk028 = get_gint32 (mhii->unknown4, ctx->byte_order); artwork->rating = get_gint32 (mhii->rating, ctx->byte_order); artwork->unk036 = get_gint32 (mhii->unknown6, ctx->byte_order); mactime = get_gint32 (mhii->orig_date, ctx->byte_order); artwork->creation_date = device_time_mac_to_time_t (device, mactime); mactime = get_gint32 (mhii->digitized_date, ctx->byte_order); artwork->digitized_date = device_time_mac_to_time_t (device, mactime); artwork->artwork_size = get_gint32 (mhii->orig_img_size, ctx->byte_order); artwork->dbid = get_gint64 (mhii->song_id, ctx->byte_order); thumbs = (Itdb_Thumb_Ipod *)itdb_thumb_ipod_new (); artwork->thumbnail = (Itdb_Thumb *)thumbs; cur_offset = ctx->header_len; mhod_ctx = db_parse_context_get_sub_context (ctx, cur_offset); num_children = get_gint32 (mhii->num_children, ctx->byte_order); while ((num_children > 0) && (mhod_ctx != NULL)) { parse_photo_mhod (mhod_ctx, thumbs, NULL); num_children--; cur_offset += mhod_ctx->total_len; g_free (mhod_ctx); mhod_ctx = db_parse_context_get_sub_context (ctx, cur_offset); } g_free (mhod_ctx); switch (ctx->db->db_type) { case DB_TYPE_PHOTO: photodb = db_get_photodb (ctx->db); g_return_val_if_fail (photodb, -1); photodb->photos = g_list_append (photodb->photos, artwork); break; case DB_TYPE_ITUNES: g_return_val_if_fail (ctx->artwork!=NULL, -1); *ctx->artwork = g_list_prepend (*ctx->artwork, artwork); break; default: g_return_val_if_reached (-1); } return 0; } static int parse_mhba (DBParseContext *ctx, GError *error) { MhbaHeader *mhba; DBParseContext *mhod_ctx; DBParseContext *mhia_ctx; Itdb_PhotoAlbum *album; Itdb_PhotoDB *photodb; int num_children; off_t cur_offset; mhba = db_parse_context_get_m_header (ctx, MhbaHeader, "mhba"); if (mhba == NULL) { return -1; } db_parse_context_set_total_len (ctx, get_gint32 (mhba->total_len, ctx->byte_order)); dump_mhba (mhba); album = g_new0 (Itdb_PhotoAlbum, 1); album->album_id = get_gint32(mhba->album_id, ctx->byte_order); album->unk024 = get_gint32(mhba->unk024, ctx->byte_order); album->unk028 = get_gint16(mhba->unk028, ctx->byte_order); album->album_type = mhba->album_type; album->playmusic = mhba->playmusic; album->repeat = mhba->repeat; album->random = mhba->random; album->show_titles = mhba->show_titles; album->transition_direction = mhba->transition_direction; album->slide_duration = get_gint32(mhba->slide_duration, ctx->byte_order); album->transition_duration = get_gint32(mhba->transition_duration, ctx->byte_order); album->unk044 = get_gint32(mhba->unk044, ctx->byte_order); album->unk048 = get_gint32(mhba->unk048, ctx->byte_order); album->song_id = get_gint64(mhba->song_id, ctx->byte_order); album->prev_album_id = get_gint32(mhba->prev_album_id, ctx->byte_order); cur_offset = ctx->header_len; num_children = get_gint32 (mhba->num_mhods, ctx->byte_order); mhod_ctx = db_parse_context_get_sub_context (ctx, cur_offset); while ((num_children > 0) && (mhod_ctx != NULL)) { struct ParsedMhodString *mhod; mhod = parse_mhod_string (mhod_ctx, error); if (mhod == NULL) { break; } switch (mhod->mhod_type) { /* FIXME: type==1 is album name. type==2 seems to be * the transtition type between photos, * e.g. "Dissolve". Not handled yet. */ case MHOD_ARTWORK_TYPE_ALBUM_NAME: g_free (album->name); album->name = mhod->mhod_string; g_free (mhod); break; default: g_free (mhod->mhod_string); g_free (mhod); break; } cur_offset += mhod_ctx->total_len; g_free (mhod_ctx); num_children--; mhod_ctx = db_parse_context_get_sub_context (ctx, cur_offset); } g_free (mhod_ctx); mhia_ctx = db_parse_context_get_sub_context (ctx, cur_offset); num_children = get_gint32 (mhba->num_mhias, ctx->byte_order); while ((num_children > 0) && (mhia_ctx != NULL)) { parse_mhia (mhia_ctx, album, NULL); num_children--; cur_offset += mhia_ctx->total_len; g_free (mhia_ctx); mhia_ctx = db_parse_context_get_sub_context (ctx, cur_offset); } g_free (mhia_ctx); photodb = db_get_photodb (ctx->db); g_return_val_if_fail (photodb, -1); album->photodb = photodb; photodb->photoalbums = g_list_append (photodb->photoalbums, album); return 0; } static int parse_mhl (DBParseContext *ctx, GError *error, const char *id, ParseListItem parse_child) { MhlHeader *mhl; int num_children; DBParseContext *mhi_ctx; off_t cur_offset; mhl = db_parse_context_get_m_header (ctx, MhlHeader, id); if (mhl == NULL) { return -1; } dump_mhl (mhl, id); num_children = get_gint32 (mhl->num_children, ctx->byte_order); if (num_children < 0) { return -1; } cur_offset = ctx->header_len; mhi_ctx = db_parse_context_get_sub_context (ctx, cur_offset); while ((num_children > 0) && (mhi_ctx != NULL)) { if (parse_child != NULL) { parse_child (mhi_ctx, NULL); } num_children--; cur_offset += mhi_ctx->total_len; g_free (mhi_ctx); mhi_ctx = db_parse_context_get_sub_context (ctx, cur_offset); } g_free (mhi_ctx); return 0; } static int parse_mhsd (DBParseContext *ctx, GError **error) { ArtworkDB_MhsdHeader *mhsd; mhsd = db_parse_context_get_m_header (ctx, ArtworkDB_MhsdHeader, "mhsd"); if (mhsd == NULL) { return -1; } db_parse_context_set_total_len (ctx, get_gint32 (mhsd->total_len, ctx->byte_order)); dump_mhsd (mhsd); switch (get_gint16_db (ctx->db, mhsd->index)) { case MHSD_IMAGE_LIST: { DBParseContext *mhli_context; mhli_context = db_parse_context_get_next_child (ctx); parse_mhl (mhli_context, NULL, "mhli", parse_mhii); g_free (mhli_context); break; } case MHSD_ALBUM_LIST: { DBParseContext *mhla_context; mhla_context = db_parse_context_get_next_child (ctx); parse_mhl (mhla_context, NULL, "mhla", parse_mhba); g_free (mhla_context); break; } case MHSD_FILE_LIST: { DBParseContext *mhlf_context; mhlf_context = db_parse_context_get_next_child (ctx); parse_mhl (mhlf_context, NULL, "mhlf", parse_mhif); g_free (mhlf_context); break; } default: g_warning (_("Unexpected mhsd index: %d\n"), get_gint16_db (ctx->db, mhsd->index)); return -1; break; } return 0; } /* Apple introduced a new way to associate artwork. The former way * used the dbid to link each artwork (mhii) back to the track. The * new way uses the mhii id to link from each track to the mhii. Above * we only handled the former way */ static int mhfd_associate_itunesdb_artwork (DBParseContext *ctx) { GHashTable *mhii_id_hash; Itdb_iTunesDB *itdb; GList *gl; g_return_val_if_fail (ctx && ctx->artwork, -1); itdb = db_get_itunesdb (ctx->db); g_return_val_if_fail (itdb, -1); /* make a hash linking the mhii with the artwork for faster lookup */ mhii_id_hash = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)itdb_artwork_free); for (gl=*ctx->artwork; gl; gl=gl->next) { Itdb_Track *track; Itdb_Artwork *artwork=gl->data; g_return_val_if_fail (artwork, -1); g_hash_table_insert (mhii_id_hash, GINT_TO_POINTER (artwork->id), artwork); /* add Artwork to track indicated by the dbid for backward compatibility */ track = get_song_by_dbid (itdb, artwork->dbid); if (track == NULL) { gchar *strval = g_strdup_printf("%" G_GINT64_FORMAT, artwork->dbid); g_print (_("Could not find corresponding track (dbid: %s) for artwork entry.\n"), strval); g_free (strval); } else { if ((track->artwork_size + track->artwork_count) != artwork->artwork_size) { g_warning (_("iTunesDB and ArtworkDB artwork sizes inconsistent (%d+%d != %d)\n"), track->artwork_size, track->artwork_count, track->artwork->artwork_size); } itdb_artwork_free (track->artwork); track->artwork = itdb_artwork_duplicate (artwork); } } /* Now go through all the tracks and add artwork where an * mhii_link is available */ for (gl=itdb->tracks; gl; gl=gl->next) { Itdb_Track *track = gl->data; g_return_val_if_fail (track, -1); if (track->mhii_link) { Itdb_Artwork *artwork; artwork = g_hash_table_lookup (mhii_id_hash, GINT_TO_POINTER (track->mhii_link)); if (artwork) { g_return_val_if_fail (track->artwork, -1); if (track->artwork->id != track->mhii_link) { itdb_artwork_free (track->artwork); track->artwork = itdb_artwork_duplicate (artwork); } else { /* same artwork -- don't copy again */ } } else { gchar *strval = g_strdup_printf("%" G_GINT64_FORMAT, track->dbid); g_print (_("Could not find artwork entry (mhii id: %u) for track (dbid: %s).\n"), track->mhii_link, strval); g_free (strval); /* couldn't find artwork -- make sure track data is in a consistent state. */ itdb_track_remove_thumbnails (track); } } } g_hash_table_destroy (mhii_id_hash); /* The actual ItdbArtwork data was freed through the GHashTable value_destroy_func */ g_list_free (*ctx->artwork); ctx->artwork = NULL; return 0; } /* Database Object */ static int parse_mhfd (DBParseContext *ctx, GError **error) { MhfdHeader *mhfd; DBParseContext *mhsd_context; unsigned int cur_pos; gint total_len; gint32 i; GList *artwork_glist = NULL; mhfd = db_parse_context_get_m_header (ctx, MhfdHeader, "mhfd"); if (mhfd == NULL) { return -1; } /* Sanity check */ total_len = get_gint32_db (ctx->db, mhfd->total_len); g_return_val_if_fail (total_len == ctx->total_len, -1); dump_mhfd (mhfd); cur_pos = ctx->header_len; if (ctx->db->db_type == DB_TYPE_ITUNES) { /* we need to collect all artwork in this GList for iTunesDB (see below) */ ctx->artwork = &artwork_glist; } for (i=0; inum_children; ++i) { /* so far all mhfd we know have 3 children, but better be safe than sorry */ mhsd_context = db_parse_context_get_sub_context (ctx, cur_pos); if (mhsd_context == NULL) { return -1; } parse_mhsd (mhsd_context, NULL); cur_pos += mhsd_context->total_len; g_free (mhsd_context); } if (ctx->db->db_type == DB_TYPE_ITUNES) { return mhfd_associate_itunesdb_artwork (ctx); } return 0; } G_GNUC_INTERNAL char * ipod_db_get_artwork_db_path (const char *mount_point) { gchar *filename=NULL; /* fail silently if no mount point given */ if (!mount_point) return NULL; filename = itdb_get_artworkdb_path (mount_point); /* itdb_resolve_path() only returns existing paths */ if (!filename) { gchar *artwork_dir; artwork_dir = itdb_get_artwork_dir (mount_point); if (!artwork_dir) { /* attempt to create Artwork dir */ gchar *control_dir = itdb_get_control_dir (mount_point); if (control_dir) { gchar *dir = g_build_filename (control_dir, "Artwork", NULL); mkdir (dir, 0777); g_free (control_dir); g_free (dir); artwork_dir = itdb_get_artwork_dir (mount_point); } } if (artwork_dir) { filename = g_build_filename (artwork_dir, "ArtworkDB", NULL); g_free (artwork_dir); } } return filename; } int ipod_parse_artwork_db (Itdb_iTunesDB *itdb) { DBParseContext *ctx; char *filename; Itdb_DB db; db.db.itdb = itdb; db.db_type = DB_TYPE_ITUNES; g_return_val_if_fail (itdb, -1); if (!itdb_device_supports_artwork (itdb->device)) { return -1; } ctx = NULL; filename = ipod_db_get_artwork_db_path (itdb_get_mountpoint (itdb)); if (filename == NULL) { goto error; } if (!g_file_test (filename, G_FILE_TEST_EXISTS)) { goto error; } ctx = db_parse_context_new_from_file (filename, &db); g_free (filename); if (ctx == NULL) { goto error; } parse_mhfd (ctx, NULL); db_parse_context_destroy (ctx); return 0; error: if (ctx != NULL) { db_parse_context_destroy (ctx); } return -1; } G_GNUC_INTERNAL char * ipod_db_get_photos_db_path (const char *mount_point) { gchar *filename=NULL; /* fail silently if no mount point given */ if (!mount_point) return NULL; filename = itdb_get_photodb_path (mount_point); /* itdb_resolve_path() only returns existing paths */ if (!filename) { gchar *photos_dir; photos_dir = itdb_get_photos_dir (mount_point); if (!photos_dir) { /* attempt to create Photos dir */ gchar *dir = g_build_filename (mount_point, "Photos", NULL); mkdir (dir, 0777); g_free (dir); photos_dir = itdb_get_photos_dir (mount_point); } if (photos_dir) { filename = g_build_filename (photos_dir, "Photo Database", NULL); g_free (photos_dir); } } return filename; } int ipod_parse_photo_db (Itdb_PhotoDB *photodb) { DBParseContext *ctx; char *filename; Itdb_DB db; GList *gl; GHashTable *hash; db.db.photodb = photodb; db.db_type = DB_TYPE_PHOTO; filename = itdb_get_photodb_path ( itdb_photodb_get_mountpoint (photodb)); if (filename == NULL) { return -1; } ctx = db_parse_context_new_from_file (filename, &db ); g_free (filename); if (ctx == NULL) { return -1; } parse_mhfd (ctx, NULL); db_parse_context_destroy (ctx); /* Now we need to replace references to artwork_ids in the * photo albums with references to the actual artwork * structure. Since we cannot guarantee that the list with the * photos is read before the album list, we cannot safely do * this at the time of reading the ids. */ /* Create a hash for faster lookup */ hash = g_hash_table_new (g_int_hash, g_int_equal); for (gl=photodb->photos; gl; gl=gl->next) { Itdb_Artwork *photo = gl->data; g_return_val_if_fail (photo, -1); g_hash_table_insert (hash, &photo->id, photo); /* printf ("id: %d, photo: %p\n", photo->id, photo);*/ } for (gl=photodb->photoalbums; gl; gl=gl->next) { GList *glp; Itdb_PhotoAlbum *album = gl->data; g_return_val_if_fail (album, -1); for (glp=album->members; glp; glp=glp->next) { guint image_id = GPOINTER_TO_UINT (glp->data); Itdb_Artwork *photo = g_hash_table_lookup (hash, &image_id); /* printf ("id: %d, photo: %p\n", image_id, photo);*/ glp->data = photo; } } g_hash_table_destroy (hash); return 0; }