diff options
author | Christophe Fergeau <teuf@gnome.org> | 2005-09-10 08:34:34 +0000 |
---|---|---|
committer | Christophe Fergeau <teuf@gnome.org> | 2005-09-10 08:34:34 +0000 |
commit | 0ab16882c390a86d28e26475b4a5e103e1e1c928 (patch) | |
tree | 40a0bf871909c8a32c2fd12dd9e137dd960dc68b /src/itdb_itunesdb.c | |
parent | e0b984b1f54de9ecd6bbf5b3f0e31e8677de3d54 (diff) | |
download | libgpod-0ab16882c390a86d28e26475b4a5e103e1e1c928.tar.gz libgpod-0ab16882c390a86d28e26475b4a5e103e1e1c928.tar.xz libgpod-0ab16882c390a86d28e26475b4a5e103e1e1c928.zip |
Initial import
git-svn-id: https://gtkpod.svn.sf.net/svnroot/gtkpod/libgpod/trunk@1080 f01d2545-417e-4e96-918e-98f8d0dbbcb6
Diffstat (limited to 'src/itdb_itunesdb.c')
-rw-r--r-- | src/itdb_itunesdb.c | 3622 |
1 files changed, 3622 insertions, 0 deletions
diff --git a/src/itdb_itunesdb.c b/src/itdb_itunesdb.c new file mode 100644 index 0000000..f039dee --- /dev/null +++ b/src/itdb_itunesdb.c @@ -0,0 +1,3622 @@ +/* Time-stamp: <2005-08-29 23:23:59 jcs> +| +| Copyright (C) 2002-2005 Jorg Schuler <jcsjcs at users sourceforge net> +| Part of the gtkpod project. +| +| URL: http://www.gtkpod.org/ +| URL: http://gtkpod.sourceforge.net/ +| +| Much of the code in this file has originally been ported from the +| perl script "mktunes.pl" (part of the gnupod-tools collection) +| written by Adrian Ulrich <pab at blinkenlights.ch>. +| +| gnupod-tools: http://www.blinkenlights.ch/cgi-bin/fm.pl?get=ipod +| +| 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! +| +| $Id$ +*/ + +/* Some notes on how to use the functions in this file: + + + *** Reading the iTunesDB *** + + Itdb_iTunesDB *itunesdb_parse (gchar *path); /+ path to mountpoint +/ + will read an Itdb_iTunesDB and pass the data over to your program. + + The information given in the "Play Counts" file is also read if + available and the playcounts, star rating and the time last played + is updated. + + Itdb_iTunesDB is a structure containing a GList for the tracks and a + GList for the playlists. + + For each track these fields are set as follows: + + "transferred" will be set to TRUE because all tracks read from a + Itdb_iTunesDB are obviously (or hopefully) already transferred to the + iPod. + + "recent_playcount" is for information only (it will allow to + generate playlists with tracks played since the last time) and will + not be stored to the iPod. + + The master playlist is guaranteed to be the first playlist, and + this must not be changed by your code. + + + *** Writing the Itdb_iTunesDB *** + + gboolean itunesdb_write (gchar *path, Itdb_iTunesDB *itb) + /+ @path to mountpoint, itb to @write +/ + will write an updated version of the Itdb_iTunesDB. + + The "Play Counts" file is renamed to "Play Counts.bak" if it exists + to avoid reading it multiple times. + + Please note that non-transferred tracks are not automatically + transferred to the iPod. A function + + gboolean itunesdb_copy_track_to_ipod (gchar *path, Itdb_Track *track, gchar *pcfile) + + is provided to help you do that, however. + + The following functions most likely will be useful: + + Itdb_Track *itunesdb_new_track (void); + Use itunesdb_new_track() to get an "initialized" track structure + (the "unknowns" are initialized with reasonable values). + + gboolean itunesdb_cp (gchar *from_file, gchar *to_file); + void itunesdb_convert_filename_fs2ipod(gchar *ipod_file); + void itunesdb_convert_filename_ipod2fs(gchar *ipod_file); + + guint32 itunesdb_time_get_mac_time (void); + time_t itunesdb_time_mac_to_host (guint32 mactime); + guint32 itunesdb_time_host_to_mac (time_t time); + + void itunesdb_rename_files (const gchar *dirname); + + (Renames/removes some files on the iPod (Playcounts, OTG + semaphore). Needs to be called if you write the Itdb_iTunesDB not + directly to the iPod but to some other location and then manually + copy the file from there to the iPod. That's much faster in the + case of using an iPod mounted in sync'ed mode.) + + Jorg Schuler, 29.12.2004 */ + + +/* call itdb_parse () to read the Itdb_iTunesDB */ +/* call itdb_write () to write the Itdb_iTunesDB */ + + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <time.h> +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> +#include <stdio.h> +#include "itdb_private.h" +#include <glib/gi18n-lib.h> + +#define ITUNESDB_DEBUG 0 +#define ITUNESDB_MHIT_DEBUG 0 + +#define ITUNESDB_COPYBLK 262144 /* blocksize for cp () */ + + +enum MHOD_ID { + MHOD_ID_TITLE = 1, + MHOD_ID_PATH = 2, + MHOD_ID_ALBUM = 3, + MHOD_ID_ARTIST = 4, + MHOD_ID_GENRE = 5, + MHOD_ID_FDESC = 6, + MHOD_ID_COMMENT = 8, + MHOD_ID_COMPOSER = 12, + MHOD_ID_GROUPING = 13, + MHOD_ID_SPLPREF = 50, /* settings for smart playlist */ + MHOD_ID_SPLRULES = 51, /* rules for smart playlist */ + MHOD_ID_MHYP = 52, /* unknown */ + MHOD_ID_PLAYLIST = 100 +}; + + +/* ID for error domain */ +GQuark itdb_file_error_quark (void) +{ + static GQuark q = 0; + if (q == 0) + q = g_quark_from_static_string ("itdb-file-error-quark"); + return q; +} + +/* Get length of utf16 string in number of characters (words) */ +static guint32 utf16_strlen (gunichar2 *utf16) +{ + guint32 i=0; + if (utf16) + while (utf16[i] != 0) ++i; + return i; +} + + +/* Read the contents of @filename and return a FContents + struct. Returns NULL in case of error and @error is set + accordingly */ +static FContents *fcontents_read (const gchar *fname, GError **error) +{ + FContents *cts; + + g_return_val_if_fail (fname, NULL); + + cts = g_new0 (FContents, 1); + + if (g_file_get_contents (fname, &cts->contents, &cts->length, error)) + { + cts->filename = g_strdup (fname); + } + else + { + g_free (cts); + cts = NULL; + } + return cts; +} + + +/* Frees the memory taken by a FContents structure. NULL pointer will + * be ignored */ +static void fcontents_free (FContents *cts) +{ + if (cts) + { + g_free (cts->filename); + g_free (cts->contents); + /* must not g_error_free (cts->error) because the error was + propagated -> might free the error twice */ + g_free (cts); + } +} + + +/* There seems to be a problem with some distributions (kernel + versions or whatever -- even identical version numbers don't don't + show identical behaviour...): even though vfat is supposed to be + case insensitive, a difference is made between upper and lower case + under some special circumstances. As in "/iPod_Control/Music/F00" + and "/iPod_Control/Music/f00 "... If the former filename does not + exist, we try to find an existing case insensitive match for each + component of the filename. If we can find such a match, we return + it. Otherwise, we return NULL.*/ + +/* We start by assuming that the ipod mount point exists. Then, for + * each component c of track->ipod_path, we try to find an entry d in + * good_path that is case-insensitively equal to c. If we find d, we + * append d to good_path and make the result the new good_path. + * Otherwise, we quit and return NULL. @root: in local encoding, + * @components: in utf8 */ +gchar * itdb_resolve_path (const gchar *root, + const gchar * const * components) +{ + gchar *good_path = g_strdup(root); + guint32 i; + + if (!root) return NULL; + + for(i = 0 ; components[i] ; i++) { + GDir *cur_dir; + gchar *component_as_filename; + gchar *test_path; + gchar *component_stdcase; + const gchar *dir_file=NULL; + + /* skip empty components */ + if (strlen (components[i]) == 0) continue; + component_as_filename = + g_filename_from_utf8(components[i],-1,NULL,NULL,NULL); + test_path = g_build_filename(good_path,component_as_filename,NULL); + g_free(component_as_filename); + if(g_file_test(test_path,G_FILE_TEST_EXISTS)) { + /* This component does not require fixup */ + g_free(good_path); + good_path = test_path; + continue; + } + g_free(test_path); + component_stdcase = g_utf8_casefold(components[i],-1); + /* Case insensitively compare the current component with each entry + * in the current directory. */ + + cur_dir = g_dir_open(good_path,0,NULL); + if (cur_dir) while ((dir_file = g_dir_read_name(cur_dir))) + { + gchar *file_utf8 = g_filename_to_utf8(dir_file,-1,NULL,NULL,NULL); + gchar *file_stdcase = g_utf8_casefold(file_utf8,-1); + gboolean found = !g_utf8_collate(file_stdcase,component_stdcase); + gchar *new_good_path; + g_free(file_stdcase); + if(!found) + { + /* This is not the matching entry */ + g_free(file_utf8); + continue; + } + + new_good_path = dir_file ? g_build_filename(good_path,dir_file,NULL) : NULL; + g_free(good_path); + good_path= new_good_path; + /* This is the matching entry, so we can stop searching */ + break; + } + + if(!dir_file) { + /* We never found a matching entry */ + g_free(good_path); + good_path = NULL; + } + + g_free(component_stdcase); + if (cur_dir) g_dir_close(cur_dir); + if(!good_path || !g_file_test(good_path,G_FILE_TEST_EXISTS)) + break; /* We couldn't fix this component, so don't try later ones */ + } + + if(good_path && g_file_test(good_path,G_FILE_TEST_EXISTS)) + return good_path; + + return NULL; +} + + +/* Check if the @seek with length @len is legal or out of + * range. Returns TRUE if legal and FALSE when it is out of range, in + * which case cts->error is set as well. */ +static gboolean check_seek (FContents *cts, glong seek, glong len) +{ + g_return_val_if_fail (cts, FALSE); + g_return_val_if_fail (cts->contents, FALSE); + + if ((seek+len <= cts->length) && (seek >=0)) + { + return TRUE; + } + else + { + g_return_val_if_fail (cts->filename, FALSE); + g_set_error (&cts->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_SEEK, + _("Illegal seek to offset %ld (length %ld) in file '%s'."), + seek, len, cts->filename); + return FALSE; + } +} + + +/* Copies @len bytes from position @seek in @cts->contents to + @data. Returns FALSE on error and sets cts->error accordingly. */ +static gboolean seek_get_n_bytes (FContents *cts, gchar *data, + glong seek, glong len) +{ + if (check_seek (cts, seek, len)) + { + memcpy (data, &cts->contents[seek], len); + return TRUE; + } + return FALSE; +} + +/* Compare @n bytes of @cts->contents starting at @seek and + * @data. Returns TRUE if equal, FALSE if not. Also returns FALSE on + * error, so you must check cts->error */ +static gboolean cmp_n_bytes_seek (FContents *cts, gchar *data, + glong seek, glong len) +{ + if (check_seek (cts, seek, len)) + { + gint i; + for (i=0; i<len; ++i) + { + if (cts->contents[seek+i] != data[i]) return FALSE; + } + return TRUE; + } + return FALSE; +} + + +/* Returns the 1-byte number stored at position @seek. On error the + * GError in @cts is set. */ +static guint8 get8int (FContents *cts, glong seek) +{ + guint8 n=0; + + if (check_seek (cts, seek, 1)) + { + n = cts->contents[seek]; + } + return n; +} + + +/* Get the 4-byte-number stored at position "seek" in little endian + encoding. On error the GError in @cts is set. */ +static guint32 get32lint (FContents *cts, glong seek) +{ + guint32 n=0; + + if (check_seek (cts, seek, 4)) + { + g_return_val_if_fail (cts->contents, 0); + memcpy (&n, &cts->contents[seek], 4); +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + n = GUINT32_SWAP_LE_BE (n); +# endif + } + return n; +} + + +/* Get the 4-byte-number stored at position "seek" in big endian + encoding. On error the GError in @cts is set. */ +static guint32 get32bint (FContents *cts, glong seek) +{ + guint32 n=0; + + if (check_seek (cts, seek, 4)) + { + g_return_val_if_fail (cts->contents, 0); + memcpy (&n, &cts->contents[seek], 4); +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + n = GUINT32_SWAP_LE_BE (n); +# endif + } + return n; +} + +/* Get the 8-byte-number stored at position "seek" in little endian + encoding. On error the GError in @cts is set. */ +static guint64 get64lint (FContents *cts, glong seek) +{ + guint64 n=0; + + if (check_seek (cts, seek, 8)) + { + g_return_val_if_fail (cts->contents, 0); + memcpy (&n, &cts->contents[seek], 8); +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + n = GUINT64_SWAP_LE_BE (n); +# endif + } + return n; +} + + +/* Get the 8-byte-number stored at position "seek" in big endian + encoding. On error the GError in @cts is set. */ +static guint64 get64bint (FContents *cts, glong seek) +{ + guint64 n=0; + + if (check_seek (cts, seek, 8)) + { + g_return_val_if_fail (cts->contents, 0); + memcpy (&n, &cts->contents[seek], 8); +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + n = GUINT64_SWAP_LE_BE (n); +# endif + } + return n; +} + +/* Fix little endian UTF16 String to correct byteorder if necessary + * (all strings in the Itdb_iTunesDB are little endian except for the ones + * in smart playlists). */ +static gunichar2 *fixup_little_utf16 (gunichar2 *utf16_string) +{ +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + gint32 i; + if (utf16_string) + { + for(i=0; i<utf16_strlen(utf16_string); i++) + { + utf16_string[i] = GUINT16_SWAP_LE_BE (utf16_string[i]); + } + } +# endif + return utf16_string; +} + +/* Fix big endian UTF16 String to correct byteorder if necessary (only + * strings in smart playlists are big endian) */ +static gunichar2 *fixup_big_utf16 (gunichar2 *utf16_string) +{ +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + gint32 i; + if (utf16_string) + { + for(i=0; i<utf16_strlen(utf16_string); i++) + { + utf16_string[i] = GUINT16_SWAP_LE_BE (utf16_string[i]); + } + } +# endif + return utf16_string; +} + + +#define CHECK_ERROR(imp, val) if (cts->error) { g_propagate_error (&imp->error, cts->error); return (val); } + + +/* get next playcount, that is the first entry of GList + * playcounts. This entry is removed from the list. You must free the + * return value after use */ +static struct playcount *playcount_get_next (FImport *fimp) +{ + struct playcount *playcount; + g_return_val_if_fail (fimp, NULL); + + playcount = g_list_nth_data (fimp->playcounts, 0); + + if (playcount) + fimp->playcounts = g_list_remove (fimp->playcounts, playcount); + return playcount; +} + +/* delete all entries of GList *playcounts */ +static void playcounts_free (FImport *fimp) +{ + struct playcount *playcount; + + g_return_if_fail (fimp); + + while ((playcount=playcount_get_next (fimp))) g_free (playcount); +} + + +/* called by init_playcounts */ +static gboolean playcounts_read (FImport *fimp, FContents *cts) +{ + guint32 header_length, entry_length, entry_num, i=0; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (cts, FALSE); + + if (!cmp_n_bytes_seek (cts, "mhdp", 0, 4)) + { + if (cts->error) + { + g_propagate_error (&fimp->error, cts->error); + } + else + { /* set error */ + g_return_val_if_fail (cts->filename, FALSE); + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Not a Play Counts file: '%s' (missing mhdp header)."), + cts->filename); + } + return FALSE; + } + header_length = get32lint (cts, 4); + CHECK_ERROR (fimp, FALSE); + /* all the headers I know are 0x60 long -- if this one is longer + we can simply ignore the additional information */ + if (header_length < 0x60) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Play Counts file ('%s'): header length smaller than expected (%d<96)."), + cts->filename, header_length); + return FALSE; + } + entry_length = get32lint (cts, 8); + CHECK_ERROR (fimp, FALSE); + /* all the entries I know are 0x0c (firmware 1.3) or 0x10 + * (firmware 2.0) or 0x14 (iTunesDB version 0x0d) in length */ + if (entry_length < 0x0c) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Play Counts file ('%s'): entry length smaller than expected (%d<12)."), + cts->filename, entry_length); + return FALSE; + } + /* number of entries */ + entry_num = get32lint (cts, 12); + CHECK_ERROR (fimp, FALSE); + for (i=0; i<entry_num; ++i) + { + struct playcount *playcount = g_new0 (struct playcount, 1); + glong seek = header_length + i*entry_length; + + fimp->playcounts = g_list_append (fimp->playcounts, playcount); + playcount->playcount = get32lint (cts, seek); + CHECK_ERROR (fimp, FALSE); + playcount->time_played = get32lint (cts, seek+4); + CHECK_ERROR (fimp, FALSE); + playcount->bookmark_time = get32lint (cts, seek+8); + CHECK_ERROR (fimp, FALSE); + /* NOTE: + * + * The iPod (firmware 1.3, 2.0, ...?) doesn't seem to use the + * timezone information correctly -- no matter what you set + * iPod's timezone to, it will always record as if it were set + * to UTC -- we need to subtract the difference between + * current timezone and UTC to get a correct + * display. -- this should be done by the application were + * necessary */ + + /* rating only exists if the entry length is at least 0x10 */ + if (entry_length >= 0x10) + { + playcount->rating = get32lint (cts, seek+12); + CHECK_ERROR (fimp, FALSE); + } + else + { + playcount->rating = NO_PLAYCOUNT; + } + /* unk16 only exists if the entry length is at least 0x14 */ + if (entry_length >= 0x14) + { + playcount->unk16 = get32lint (cts, seek+16); + CHECK_ERROR (fimp, FALSE); + } + else + { + playcount->unk16 = 0; + } + } + return TRUE; +} + + + +/* Read the Play Count file (formed by adding "Play Counts" to the + * directory component of fimp->itdb->itdb_filename) and set up the + * GList *playcounts. + * Returns TRUE on success (also when no Play Count + * file is found as this is not an error) and FALSE otherwise, in + * which case fimp->error is set accordingly. */ +static gboolean playcounts_init (FImport *fimp) +{ + const gchar *db[] = {"Play Counts", NULL}; + gchar *plcname, *dirname; + gboolean result=FALSE; + struct stat filestat; + FContents *cts; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (!fimp->error, FALSE); + g_return_val_if_fail (!fimp->playcounts, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + g_return_val_if_fail (fimp->itdb->filename, FALSE); + + dirname = g_path_get_dirname (fimp->itdb->filename); + + plcname = itdb_resolve_path (dirname, db); + + g_free (dirname); + + /* skip if no playcounts file is present */ + if (!plcname) return TRUE; + + /* skip if playcounts file has zero-length (often happens after + * dosfsck) */ + stat (plcname, &filestat); + if (filestat.st_size < 0x60) return TRUE; /* check for header length */ + + cts = fcontents_read (plcname, &fimp->error); + if (cts) + { + result = playcounts_read (fimp, cts); + fcontents_free (cts); + } + g_free (plcname); + return result; +} + + +/* Free the memory taken by @fimp. fimp->itdb must be freed separately + * before calling this function */ +static void itdb_free_fimp (FImport *fimp) +{ + if (fimp) + { + if (fimp->itunesdb) fcontents_free (fimp->itunesdb); + g_list_free (fimp->pos_glist); + playcounts_free (fimp); + g_free (fimp); + } +} + +/* Free the memory taken by @itdb. */ +void itdb_free (Itdb_iTunesDB *itdb) +{ + if (itdb) + { + g_list_foreach (itdb->playlists, + (GFunc)(itdb_playlist_free), NULL); + g_list_free (itdb->playlists); + g_list_foreach (itdb->tracks, + (GFunc)(itdb_track_free), NULL); + g_list_free (itdb->tracks); + g_free (itdb->filename); + g_free (itdb->mountpoint); + if (itdb->userdata && itdb->userdata_destroy) + (*itdb->userdata_destroy) (itdb->userdata); + g_free (itdb); + } +} + +/* Free the memory taken by @itdb. */ +Itdb_iTunesDB *itdb_duplicate (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb, NULL); + g_return_val_if_fail (!itdb->userdata || + itdb->userdata_duplicate, NULL); + /* FIXME: not yet implemented */ + g_return_val_if_reached (NULL); +} + +/* return number of playlists */ +guint32 itdb_playlists_number (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb, 0); + + return g_list_length (itdb->playlists); +} + + +/* return total number of tracks */ +guint32 itdb_tracks_number (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb, 0); + + return g_list_length (itdb->tracks); +} + + +guint32 itdb_tracks_number_nontransferred (Itdb_iTunesDB *itdb) +{ + guint n = 0; + GList *gl; + g_return_val_if_fail (itdb, 0); + + for (gl=itdb->tracks; gl; gl=gl->next) + { + Itdb_Track *track = gl->data; + g_return_val_if_fail (track, 0); + if (!track->transferred) ++n; + } + return n; +} + + + +/* Creates a new Itdb_iTunesDB with the unknowns filled in to reasonable + values */ +Itdb_iTunesDB *itdb_new (void) +{ + GRand *grand = g_rand_new (); + + Itdb_iTunesDB *itdb = g_new0 (Itdb_iTunesDB, 1); + itdb->version = 0x09; + itdb->id = ((guint64)g_rand_int (grand) << 32) | + ((guint64)g_rand_int (grand)); + return itdb; +} + +/* Returns the type of the mhod and the length *ml. *ml is set to -1 + * on error (e.g. because there's no mhod at @seek). */ +/* A return value of -1 and no error set means that no mhod was found + at @seek */ +static gint32 get_mhod_type (FContents *cts, glong seek, gint32 *ml) +{ + gint32 type = -1; + +#if ITUNESDB_DEBUG + fprintf(stderr, "get_mhod_type seek: %x\n", (int)seek); +#endif + + if (ml) *ml = -1; + + if (cmp_n_bytes_seek (cts, "mhod", seek, 4)) + { + guint32 len = get32lint (cts, seek+8); /* total length */ + if (cts->error) return -1; + if (ml) *ml = len; + type = get32lint (cts, seek+12); /* mhod_id */ + if (cts->error) return -1; + } + return type; +} + +/* Returns a pointer to the data contained in the mhod at position + @seek. This can be a simple string or something more complicated as + in the case for SPLPREF or SPLRULES. *ml is set to the total length + of the mhod (-1 in case of an error), *mty is set to the type of + the mhod. + On error NULL is returned and cts->error is set appropriately. */ +static void *get_mhod (FContents *cts, gulong mhod_seek, + gint32 *ml, gint32 *mty) +{ + gunichar2 *entry_utf16 = NULL; + SPLPref *splp = NULL; + guint8 limitsort_opposite; + void *result = NULL; + gint32 xl, len; + gint32 header_length; + gulong seek; + + g_return_val_if_fail (ml, NULL); + g_return_val_if_fail (mty, NULL); + g_return_val_if_fail (cts, NULL); + +#if ITUNESDB_DEBUG + fprintf(stderr, "get_mhod seek: %ld\n", mhod_seek); +#endif + + g_return_val_if_fail (cts, NULL); + + *ml = -1; + + g_return_val_if_fail (!cts->error, NULL); + + *mty = get_mhod_type (cts, mhod_seek, &len); + if (*mty == -1) + { + if (!cts->error) + { /* set error */ + g_set_error (&cts->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: no MHOD at offset %ld in file '%s'."), + mhod_seek, cts->filename); + } + return NULL; + } + header_length = get32lint (cts, mhod_seek+4); /* header length */ + if (cts->error) return NULL; + + seek = mhod_seek + header_length; + +#if ITUNESDB_DEBUG + fprintf(stderr, "ml: %x mty: %x\n", *ml, *mty); +#endif + + switch ((enum MHOD_ID)*mty) + { + case MHOD_ID_MHYP: + /* this is not yet supported */ + case MHOD_ID_PLAYLIST: + /* return the position indicator */ + result = (void *)get32lint (cts, mhod_seek+24); + if (cts->error) return NULL; + break; + case MHOD_ID_TITLE: + case MHOD_ID_PATH: + case MHOD_ID_ALBUM: + case MHOD_ID_ARTIST: + case MHOD_ID_GENRE: + case MHOD_ID_FDESC: + case MHOD_ID_COMMENT: + case MHOD_ID_COMPOSER: + case MHOD_ID_GROUPING: + xl = get32lint (cts, seek+4); /* entry length */ + if (cts->error) return NULL; + entry_utf16 = g_new0 (gunichar2, (xl+2)/2); + if (seek_get_n_bytes (cts, (gchar *)entry_utf16, seek+16, xl)) + { + result = fixup_little_utf16 (entry_utf16); + } + else + { /* error */ + g_free (entry_utf16); + return NULL; + } + break; + case MHOD_ID_SPLPREF: /* Settings for smart playlist */ + splp = g_new0 (SPLPref, 1); + splp->liveupdate = get8int (cts, seek); + if (cts->error) return NULL; + splp->checkrules = get8int (cts, seek+1); + if (cts->error) return NULL; + splp->checklimits = get8int (cts, seek+2); + if (cts->error) return NULL; + splp->limittype = get8int (cts, seek+3); + if (cts->error) return NULL; + splp->limitsort = get8int (cts, seek+4); + if (cts->error) return NULL; + splp->limitvalue = get32lint (cts, seek+8); + if (cts->error) return NULL; + splp->matchcheckedonly = get8int (cts, seek+12); + if (cts->error) return NULL; + limitsort_opposite = get8int (cts, seek+13); + if (cts->error) return NULL; + /* if the opposite flag is on, set limitsort's high bit -- see + note in itunesdb.h for more info */ + if (limitsort_opposite) + splp->limitsort |= 0x80000000; + result = splp; + break; + case MHOD_ID_SPLRULES: /* Rules for smart playlist */ + if (cmp_n_bytes_seek (cts, "SLst", seek, 4)) + { + /* !!! for some reason the SLst part is the only part of the + iTunesDB with big-endian encoding, including UTF16 + strings */ + gint i; + guint32 numrules; + SPLRules *splrs = g_new0 (SPLRules, 1); + splrs->unk004 = get32bint (cts, seek+4); + if (cts->error) return NULL; + numrules = get32bint (cts, seek+8); + if (cts->error) return NULL; + splrs->match_operator = get32bint (cts, seek+12); + if (cts->error) return NULL; + seek += 136; /* I can't find this value stored in the + iTunesDB :-( */ + for (i=0; i<numrules; ++i) + { + guint32 length; + SPLRule *splr = g_new0 (SPLRule, 1); + splr->field = get32bint (cts, seek); + if (cts->error) return NULL; + splr->action = get32bint (cts, seek+4); + if (cts->error) return NULL; + seek += 52; + length = get32bint (cts, seek); + if (cts->error) return NULL; + if (itdb_spl_action_known (splr->action)) + { + gint ft = itdb_splr_get_field_type (splr); + if (ft == splft_string) + { + gunichar2 *string_utf16 = g_new0 (gunichar2, + (length+2)/2); + if (!seek_get_n_bytes (cts, (gchar *)string_utf16, + seek+4, length)) + { + g_free (string_utf16); + g_free (splr); + return NULL; + } + fixup_big_utf16 (string_utf16); + splr->string = g_utf16_to_utf8 ( + string_utf16, -1, NULL, NULL, NULL); + g_free (string_utf16); + } + else + { + if (length != 0x44) + { + g_warning (_("Length of smart playlist rule field (%d) not as expected. Trying to continue anyhow.\n"), length); + } + splr->fromvalue = get64bint (cts, seek+4); + if (cts->error) return NULL; + splr->fromdate = get64bint (cts, seek+12); + if (cts->error) return NULL; + splr->fromunits = get64bint (cts, seek+20); + if (cts->error) return NULL; + splr->tovalue = get64bint (cts, seek+28); + if (cts->error) return NULL; + splr->todate = get64bint (cts, seek+36); + if (cts->error) return NULL; + splr->tounits = get64bint (cts, seek+44); + if (cts->error) return NULL; + /* SPLFIELD_PLAYLIST seem to use these unknowns*/ + splr->unk052 = get32bint (cts, seek+52); + if (cts->error) return NULL; + splr->unk056 = get32bint (cts, seek+56); + if (cts->error) return NULL; + splr->unk060 = get32bint (cts, seek+60); + if (cts->error) return NULL; + splr->unk064 = get32bint (cts, seek+64); + if (cts->error) return NULL; + splr->unk068 = get32bint (cts, seek+68); + if (cts->error) return NULL; + } + seek += length+4; + } + else + { + g_free (splr); + splr = NULL; + } + if (splr) + { + splrs->rules = g_list_append (splrs->rules, splr); + } + } + result = splrs; + } + else + { + if (!cts->error) + g_warning (_("Did not find SLst hunk as expected. Trying to continue.\n")); + else + return NULL; + } + break; + default: + g_warning (_("Encountered unknown MHOD type (%d) while parsing the iTunesDB. Ignoring.\n\n"), *mty); + break; + } + *ml = len; + return result; +} + +/* Returns the value of a string type mhod. return the length of the + mhod *ml, the mhod type *mty, and a string with the entry (in + UTF16). After use you must free the string with g_free(). Returns + NULL if no string is avaible. *ml is set to -1 in case of error and + cts->error is set appropriately. */ +static gunichar2 *get_mhod_string (FContents *cts, glong seek, gint32 *ml, gint32 *mty) +{ + gunichar2 *result = NULL; + + *mty = get_mhod_type (cts, seek, ml); + if (cts->error) return NULL; + + if (*ml != -1) switch ((enum MHOD_ID)*mty) + { + case MHOD_ID_TITLE: + case MHOD_ID_PATH: + case MHOD_ID_ALBUM: + case MHOD_ID_ARTIST: + case MHOD_ID_GENRE: + case MHOD_ID_FDESC: + case MHOD_ID_COMMENT: + case MHOD_ID_COMPOSER: + case MHOD_ID_GROUPING: + result = get_mhod (cts, seek, ml, mty); + break; + case MHOD_ID_SPLPREF: + case MHOD_ID_SPLRULES: + case MHOD_ID_MHYP: + case MHOD_ID_PLAYLIST: + /* these do not have a string entry */ + break; + } + return result; +} + + +/* Get a playlist. Returns the position where the next playlist should + be. On error -1 is returned and fimp->error is set appropriately. */ +static glong get_playlist (FImport *fimp, glong seek) +{ + gint pos_comp (gpointer a, gpointer b) + { + return ((gint)a - (gint)b); + } + + gunichar2 *plname_utf16 = NULL; + guint32 i, type, tracknum, mhod_num; + glong nextseek; + guint32 hlen; + Itdb_Playlist *plitem = NULL; + FContents *cts; + +#if ITUNESDB_DEBUG + fprintf(stderr, "mhyp seek: %x\n", (int)seek); +#endif + g_return_val_if_fail (fimp, -1); + g_return_val_if_fail (fimp->idtree, -1); + + cts = fimp->itunesdb; + + if (!cmp_n_bytes_seek (cts, "mhyp", seek, 4)) + { + if (cts->error) + g_propagate_error (&fimp->error, cts->error); + return -1; + } + hlen = get32lint (cts, seek+4); /* length of header */ + CHECK_ERROR (fimp, -1); + if (hlen == 0) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: hunk length 0 for hunk at %ld in file '%s'."), + seek, cts->filename); + return -1; + } + nextseek = seek + get32lint (cts, seek+8);/* possible begin of next PL */ + CHECK_ERROR (fimp, -1); + mhod_num = get32lint (cts, seek+12); /* number of MHODs we expect */ + CHECK_ERROR (fimp, -1); + tracknum = get32lint (cts, seek+16); /* number of tracks in playlist */ + CHECK_ERROR (fimp, -1); + plitem = itdb_playlist_new (NULL, FALSE); + /* Some Playlists have added 256 to their type -- I don't know what + it's for, so we just ignore it for now -> & 0xff */ + plitem->type = get32lint (cts, seek+20) & 0xff; + CHECK_ERROR (fimp, -1); + plitem->id = get64lint (cts, seek+28); + CHECK_ERROR (fimp, -1); + plitem->unk036 = get32lint (cts, seek+36); + CHECK_ERROR (fimp, -1); + plitem->unk040 = get32lint (cts, seek+40); + CHECK_ERROR (fimp, -1); + plitem->unk044 = get32lint (cts, seek+44); + CHECK_ERROR (fimp, -1); + for (i=0; i < mhod_num; ++i) + { + gunichar2 *plname_utf16_maybe; + SPLPref *splpref = NULL; + SPLRules *splrules = NULL; + + seek += hlen; + type = get_mhod_type (cts, seek, &hlen); + CHECK_ERROR (fimp, -1); + if (hlen != -1) switch ((enum MHOD_ID)type) + { + case MHOD_ID_PLAYLIST: + /* here we could do something about the playlist settings */ + break; + case MHOD_ID_TITLE: + plname_utf16_maybe = get_mhod (cts, seek, &hlen, &type); + CHECK_ERROR (fimp, -1); + if (plname_utf16_maybe) + { + /* sometimes there seem to be two mhod TITLE headers */ + g_free (plname_utf16); + plname_utf16 = plname_utf16_maybe; + } + break; + case MHOD_ID_SPLPREF: + splpref = get_mhod (cts, seek, &hlen, &type); + CHECK_ERROR (fimp, -1); + if (splpref) + { + plitem->is_spl = TRUE; + memcpy (&plitem->splpref, splpref, sizeof (SPLPref)); + g_free (splpref); + splpref = NULL; + } + break; + case MHOD_ID_SPLRULES: + splrules = get_mhod (cts, seek, &hlen, &type); + CHECK_ERROR (fimp, -1); + if (splrules) + { + plitem->is_spl = TRUE; + memcpy (&plitem->splrules, splrules, sizeof (SPLRules)); + g_free (splrules); + splrules = NULL; + } + break; + case MHOD_ID_PATH: + case MHOD_ID_ALBUM: + case MHOD_ID_ARTIST: + case MHOD_ID_GENRE: + case MHOD_ID_FDESC: + case MHOD_ID_COMMENT: + case MHOD_ID_COMPOSER: + case MHOD_ID_GROUPING: + /* these are not expected here */ + break; + case MHOD_ID_MHYP: + /* this I don't know how to handle */ + break; + } + } + + if (plname_utf16) + { + plitem->name = g_utf16_to_utf8 (plname_utf16, -1, NULL, NULL, NULL); + g_free (plname_utf16); + } + else + { /* we did not read a valid mhod TITLE header -> */ + /* we simply make up our own name */ + if (plitem->type == ITDB_PL_TYPE_MPL) + plitem->name = _("Master-PL"); + else + plitem->name = _("Playlist"); + } + +#if ITUNESDB_DEBUG + fprintf(stderr, "pln: %s(%d Itdb_Tracks) \n", plitem->name, (int)tracknum); +#endif + + /* add new playlist */ + itdb_playlist_add (fimp->itdb, plitem, -1); + + i=0; /* tracks read */ + while (i<tracknum) + { + guint32 len = get32lint (cts, seek+8); +#if ITUNESDB_DEBUG + fprintf(stderr, " %lx: seeking track %d of %d\n", seek, i+1, (int)tracknum); +#endif + CHECK_ERROR (fimp, -1); + /* We read the mhip headers and skip everything else (the mhips + * seem to come in pairs: mhip/mhod mhip/mhod ...). */ + if (cmp_n_bytes_seek (cts, "mhyp", seek, 4)) + { /* This cannot be, let's abort... */ + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: found mhyp at %ld in file '%s'."), + seek, cts->filename); + return -1; + } + if (cmp_n_bytes_seek (cts, "mhip", seek, 4)) + { +#if ITUNESDB_DEBUG + fprintf(stderr, " %lx: mhit\n", seek); +#endif + Itdb_Track *tr; + gint32 pos = -1; + guint32 posid; + gint32 mhod_type; + gint32 mhod_len; + guint32 mhit_len; + guint32 ref; + mhit_len = get32lint(cts, seek+4); + CHECK_ERROR (fimp, -1); + ref = get32lint(cts, seek+24); + CHECK_ERROR (fimp, -1); + /* the mhod that follows gives us the position in the + playlist (type 100) */ + mhod_type = get_mhod_type (cts, seek+mhit_len, NULL); + CHECK_ERROR (fimp, -1); + if (mhod_type == MHOD_ID_PLAYLIST) + { + posid = (guint32)get_mhod (cts, seek+mhit_len, + &mhod_len, &mhod_type); + CHECK_ERROR (fimp, -1); + /* The posids don't have to be in numeric order, but our + database depends on the playlist members being sorted + according to the order they appear in the + playlist. Therefore we need to find out at which + position to insert the track */ + fimp->pos_glist = g_list_insert_sorted ( + fimp->pos_glist, (gpointer)posid, + (GCompareFunc)pos_comp); + pos = g_list_index (fimp->pos_glist, (gpointer)posid); + /* For performance reasons set pos to -1 if position is + end of list */ + if (pos == i) pos = -1; + } + tr = itdb_track_id_tree_by_id (fimp->idtree, ref); + if (tr) + { + itdb_playlist_add_track (plitem, tr, pos); + } + else + g_warning (_("Itdb_Track ID '%d' not found.\n"), ref); + ++i; + } + CHECK_ERROR (fimp, -1); + seek += len; + } + g_list_free (fimp->pos_glist); + fimp->pos_glist = NULL; + return nextseek; +} + + +/* returns a pointer to the next header or -1 on error. fimp->error is + set appropriately. If no "mhit" header is found at the location + specified, -1 is returned but no error is set. */ +static glong get_mhit (FImport *fimp, glong seek) +{ + Itdb_Track *track; + gchar *entry_utf8; + gunichar2 *entry_utf16; + gint32 type, zip; + struct playcount *playcount; + guint32 i, temp, mhod_nums; + FContents *cts; + +#if ITUNESDB_DEBUG + fprintf(stderr, "get_mhit seek: %x\n", (int)seek); +#endif + + g_return_val_if_fail (fimp, -1); + + cts = fimp->itunesdb; + + if (!cmp_n_bytes_seek (cts, "mhit", seek, 4)) + { + if (cts->error) + g_propagate_error (&fimp->error, cts->error); + return -1; + } + + mhod_nums = get32lint (cts, seek+12); + CHECK_ERROR (fimp, -1); + + track = itdb_track_new (); + + track->id = get32lint(cts, seek+16); /* iPod ID */ + CHECK_ERROR (fimp, -1); + track->unk020 = get32lint (cts, seek+20); + CHECK_ERROR (fimp, -1); + track->unk024 = get32lint (cts, seek+24); + CHECK_ERROR (fimp, -1); + temp = get32lint (cts, seek+28); + CHECK_ERROR (fimp, -1); + track->rating = (temp & 0xff000000) >> 24; /* rating */ + track->compilation = (temp & 0x00ff0000) >> 16; + track->type = temp & 0x0000ffff; + track->time_added = get32lint(cts, seek+32); /* time added */ + CHECK_ERROR (fimp, -1); + track->size = get32lint(cts, seek+36); /* file size */ + CHECK_ERROR (fimp, -1); + track->tracklen = get32lint(cts, seek+40); /* time */ + CHECK_ERROR (fimp, -1); + track->track_nr = get32lint(cts, seek+44); /* track number */ + CHECK_ERROR (fimp, -1); + track->tracks = get32lint(cts, seek+48); /* nr of tracks */ + CHECK_ERROR (fimp, -1); + track->year = get32lint(cts, seek+52); /* year */ + CHECK_ERROR (fimp, -1); + track->bitrate = get32lint(cts, seek+56); /* bitrate */ + CHECK_ERROR (fimp, -1); + track->samplerate = get32lint(cts,seek+60)>>16; /* sample rate */ + CHECK_ERROR (fimp, -1); + track->volume = get32lint(cts, seek+64); /* volume adjust */ + CHECK_ERROR (fimp, -1); + track->starttime = get32lint (cts, seek+68); + CHECK_ERROR (fimp, -1); + track->stoptime = get32lint (cts, seek+72); + CHECK_ERROR (fimp, -1); + track->soundcheck = get32lint (cts, seek+76); /* soundcheck */ + CHECK_ERROR (fimp, -1); + track->playcount = get32lint (cts, seek+80); /* playcount */ + CHECK_ERROR (fimp, -1); + track->unk084 = get32lint (cts, seek+84); + CHECK_ERROR (fimp, -1); + track->time_played = get32lint(cts, seek+88); /* last time played */ + CHECK_ERROR (fimp, -1); + track->cd_nr = get32lint(cts, seek+92); /* CD nr */ + CHECK_ERROR (fimp, -1); + track->cds = get32lint(cts, seek+96); /* CD nr of.. */ + CHECK_ERROR (fimp, -1); + track->unk100 = get32lint (cts, seek+100); + CHECK_ERROR (fimp, -1); + track->time_modified = get32lint(cts, seek+104);/* last mod. time */ + CHECK_ERROR (fimp, -1); + track->bookmark_time = get32lint (cts, seek+108); /* time bookmarked */ + CHECK_ERROR (fimp, -1); + track->dbid = get64lint (cts, seek+112); + CHECK_ERROR (fimp, -1); + temp = get32lint (cts, seek+120); + CHECK_ERROR (fimp, -1); + track->BPM = temp >> 16; + track->app_rating = (temp & 0xff00)>> 8;/* The rating set by * the + application, as opposed to + the rating set on the iPod + itself */ + track->checked = temp & 0xff; /* Checked/Unchecked: 0/1 */ + track->unk124 = get32lint (cts, seek+124); + CHECK_ERROR (fimp, -1); + track->unk128 = get32lint (cts, seek+128); + CHECK_ERROR (fimp, -1); + track->unk132 = get32lint (cts, seek+132); + CHECK_ERROR (fimp, -1); + track->unk136 = get32lint (cts, seek+136); + CHECK_ERROR (fimp, -1); + track->unk140 = get32lint (cts, seek+140); + CHECK_ERROR (fimp, -1); + track->unk144 = get32lint (cts, seek+144); + CHECK_ERROR (fimp, -1); + track->unk148 = get32lint (cts, seek+148); + CHECK_ERROR (fimp, -1); + track->unk152 = get32lint (cts, seek+152); + CHECK_ERROR (fimp, -1); + + track->transferred = TRUE; /* track is on iPod! */ + +#if ITUNESDB_MHIT_DEBUG +time_t time_mac_to_host (guint32 mactime); +gchar *time_time_to_string (time_t time); +#define printf_mhit(sk, str) printf ("%3d: %d (%s)\n", sk, get32lint (file, seek+sk), str); +#define printf_mhit_time(sk, str) { gchar *buf = time_time_to_string (itunesdb_time_mac_to_host (get32lint (file, seek+sk))); printf ("%3d: %s (%s)\n", sk, buf, str); g_free (buf); } + { + printf ("\nmhit: seek=%lu\n", seek); + printf_mhit ( 4, "header size"); + printf_mhit ( 8, "mhit size"); + printf_mhit ( 12, "nr of mhods"); + printf_mhit ( 16, "iPod ID"); + printf_mhit ( 20, "?"); + printf_mhit ( 24, "?"); + printf (" 28: %u (type)\n", get32lint (file, seek+28) & 0xffffff); + printf (" 28: %u (rating)\n", get32lint (file, seek+28) >> 24); + printf_mhit ( 32, "timestamp file"); + printf_mhit_time ( 32, "timestamp file"); + printf_mhit ( 36, "size"); + printf_mhit ( 40, "tracklen (ms)"); + printf_mhit ( 44, "track_nr"); + printf_mhit ( 48, "total tracks"); + printf_mhit ( 52, "year"); + printf_mhit ( 56, "bitrate"); + printf_mhit ( 60, "sample rate"); + printf (" 60: %u (sample rate LSB)\n", get32lint (file, seek+60) & 0xffff); + printf (" 60: %u (sample rate HSB)\n", (get32lint (file, seek+60) >> 16)); + printf_mhit ( 64, "?"); + printf_mhit ( 68, "?"); + printf_mhit ( 72, "?"); + printf_mhit ( 76, "?"); + printf_mhit ( 80, "playcount"); + printf_mhit ( 84, "?"); + printf_mhit ( 88, "last played"); + printf_mhit_time ( 88, "last played"); + printf_mhit ( 92, "CD"); + printf_mhit ( 96, "total CDs"); + printf_mhit (100, "?"); + printf_mhit (104, "?"); + printf_mhit_time (104, "?"); + printf_mhit (108, "?"); + printf_mhit (112, "?"); + printf_mhit (116, "?"); + printf_mhit (120, "?"); + printf_mhit (124, "?"); + printf_mhit (128, "?"); + printf_mhit (132, "?"); + printf_mhit (136, "?"); + printf_mhit (140, "?"); + printf_mhit (144, "?"); + printf_mhit (148, "?"); + printf_mhit (152, "?"); + } +#undef printf_mhit_time +#undef printf_mhit +#endif + + seek += get32lint (cts, seek+4); /* 1st mhod starts here! */ + CHECK_ERROR (fimp, -1); + + for (i=0; i<mhod_nums; ++i) + { + entry_utf16 = get_mhod_string (cts, seek, &zip, &type); + CHECK_ERROR (fimp, -1); + if (entry_utf16 != NULL) + { + entry_utf8 = g_utf16_to_utf8 (entry_utf16, -1, NULL, NULL, NULL); + switch ((enum MHOD_ID)type) + { + case MHOD_ID_ALBUM: + track->album = entry_utf8; + break; + case MHOD_ID_ARTIST: + track->artist = entry_utf8; + break; + case MHOD_ID_TITLE: + track->title = entry_utf8; + break; + case MHOD_ID_GENRE: + track->genre = entry_utf8; + break; + case MHOD_ID_PATH: + track->ipod_path = entry_utf8; + break; + case MHOD_ID_FDESC: + track->fdesc = entry_utf8; + break; + case MHOD_ID_COMMENT: + track->comment = entry_utf8; + break; + case MHOD_ID_COMPOSER: + track->composer = entry_utf8; + break; + case MHOD_ID_GROUPING: + track->grouping = entry_utf8; + break; + default: /* unknown entry -- discard */ + g_free (entry_utf8); + break; + } + g_free (entry_utf16); + } + seek += zip; + } + + playcount = playcount_get_next (fimp); + if (playcount) + { + if (playcount->rating != NO_PLAYCOUNT) + track->rating = playcount->rating; + + if (playcount->time_played) + track->time_played = playcount->time_played; + + if (playcount->bookmark_time) + track->bookmark_time = playcount->bookmark_time; + + track->playcount += playcount->playcount; + track->recent_playcount = playcount->playcount; + g_free (playcount); + } + itdb_track_add (fimp->itdb, track, -1); + return seek; +} + + +/* Called by read_OTG_playlists(): OTG playlist stored in @cts by + * adding a new playlist (named @plname) with the tracks specified in + * @cts. If @plname is NULL, a standard name will be substituted */ +/* Returns FALSE on error, TRUE on success. On error @fimp->error will + * be set apropriately. */ +static gboolean process_OTG_file (FImport *fimp, FContents *cts, + const gchar *plname) +{ + guint32 header_length, entry_length, entry_num; + + g_return_val_if_fail (fimp && cts, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + + if (!plname) plname = _("OTG Playlist"); + + if (!cmp_n_bytes_seek (cts, "mhpo", 0, 4)) + { + if (cts->error) + { + g_propagate_error (&fimp->error, cts->error); + } + else + { /* set error */ + g_return_val_if_fail (cts->filename, FALSE); + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Not a OTG playlist file: '%s' (missing mhpo header)."), + cts->filename); + } + return FALSE; + } + header_length = get32lint (cts, 4); + CHECK_ERROR (fimp, FALSE); + /* all the headers I know are 0x14 long -- if this one is + longer we can simply ignore the additional information */ + if (header_length < 0x14) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("OTG playlist file ('%s'): header length smaller than expected (%d<20)."), + cts->filename, header_length); + return FALSE; + } + entry_length = get32lint (cts, 8); + CHECK_ERROR (fimp, FALSE); + /* all the entries I know are 0x04 long */ + if (entry_length < 0x04) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("OTG playlist file file ('%s'): entry length smaller than expected (%d<4)."), + cts->filename, entry_length); + return FALSE; + } + /* number of entries */ + entry_num = get32lint (cts, 12); + CHECK_ERROR (fimp, FALSE); + + if (entry_num > 0) + { + gint i; + Itdb_Playlist *pl; + + pl = itdb_playlist_new (plname, FALSE); + /* Add new playlist */ + itdb_playlist_add (fimp->itdb, pl, -1); + + /* Add items */ + for (i=0; i<entry_num; ++i) + { + Itdb_Track *track; + guint32 num = get32lint (cts, + header_length + entry_length *i); + CHECK_ERROR (fimp, FALSE); + + track = g_list_nth_data (fimp->itdb->tracks, num); + if (track) + { + itdb_playlist_add_track (pl, track, -1); + } + else + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("OTG playlist file '%s': reference to non-existent track (%d)."), + cts->filename, num); + return FALSE; + } + } + } + return TRUE; +} + + + + +/* Add the On-The-Go Playlist(s) to the database */ +/* The OTG-Files are located in the directory given by + fimp->itdb->itdb_filename. + On error FALSE is returned and fimp->error is set accordingly. */ +static gboolean read_OTG_playlists (FImport *fimp) +{ + gchar *db[] = {"OTGPlaylistInfo", NULL}; + gchar *dirname, *otgname; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + g_return_val_if_fail (fimp->itdb->filename, FALSE); + + dirname = g_path_get_dirname (fimp->itdb->filename); + + otgname = itdb_resolve_path (dirname, (const gchar **)db); + + + /* only parse if "OTGPlaylistInfo" exists */ + if (otgname) + { + gchar *filename; + gint i=1; + do + { + db[0] = g_strdup_printf ("OTGPlaylistInfo_%d", i); + filename = itdb_resolve_path (dirname, (const gchar **)db); + g_free (db[0]); + if (filename) + { + FContents *cts = fcontents_read (filename, &fimp->error); + if (cts) + { + gchar *plname = g_strdup_printf (_("OTG Playlist %d"), i); + process_OTG_file (fimp, cts, plname); + g_free (plname); + fcontents_free (cts); + } + g_free (filename); + } + if (fimp->error) break; + ++i; + } while (filename); + g_free (otgname); + } + g_free (dirname); + return TRUE; +} + + + +static gboolean parse_fimp (FImport *fimp) +{ + glong seek=0, pl_mhsd=0; + guint32 i, zip, nr_tracks=0, nr_playlists=0; + gboolean swapped_mhsd = FALSE; + FContents *cts; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + g_return_val_if_fail (fimp->itunesdb, FALSE); + g_return_val_if_fail (fimp->itunesdb->filename, FALSE); + + cts = fimp->itunesdb; + + if (!cmp_n_bytes_seek (cts, "mhbd", 0, 4)) + { + if (cts->error) + { + g_propagate_error (&fimp->error, cts->error); + } + else + { /* set error */ + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Not a iTunesDB: '%s' (missing mhdb header)."), + cts->filename); + } + return FALSE; + } + seek = get32lint (cts, 4); + CHECK_ERROR (fimp, FALSE); + /* all the headers I know are 0x68 long -- if this one is longer + we can could simply ignore the additional information */ + /* Since we only need data from the first 32 bytes, don't complain + * unless it's smaller than that */ + if (seek < 32) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB ('%s'): header length of mhsd hunk smaller than expected (%ld<32). Aborting."), + cts->filename, seek); + return FALSE; + } + + fimp->itdb->version = get32lint (cts, seek+16); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->id = get64lint (cts, seek+24); + CHECK_ERROR (fimp, FALSE); + + for (;;) + { + if (cmp_n_bytes_seek (cts, "mhsd", seek, 4)) + { /* mhsd header -> determine start of playlists */ + guint32 sth; + guint32 len; + len = get32lint (cts, seek+8); + CHECK_ERROR (fimp, FALSE); + sth = get32lint (cts, seek+12); + CHECK_ERROR (fimp, FALSE); + if (sth == 1) + { /* OK, tracklist, save start of playlists */ + if (!swapped_mhsd) + pl_mhsd = seek + len; + } + else if (sth == 2) + { /* bad: these are playlists... switch */ + if (swapped_mhsd) + { /* already switched once -> forget it */ + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB '%s' corrupt: already found two playlist mhsds -- giving up."), + cts->filename); + return FALSE; + } + else + { + pl_mhsd = seek; + seek += len; + swapped_mhsd = TRUE; + } + } + else + { /* neither playlist nor track MHSD --> skip it */ + seek += len; + } + } + else + { /* if the cmp_n_bytes_seek() failed we must check if it is + because of an error */ + CHECK_ERROR (fimp, FALSE); + } + if (cmp_n_bytes_seek (cts, "mhlt", seek, 4)) + { /* mhlt header -> number of tracks */ + nr_tracks = get32lint (cts, seek+8); + CHECK_ERROR (fimp, FALSE); + if (nr_tracks == 0) + { /* no tracks -- skip directly to next mhsd */ + break; + } + } + else + { /* if the cmp_n_bytes_seek() failed we must check if it is + because of an error */ + CHECK_ERROR (fimp, FALSE); + } + if (cmp_n_bytes_seek (cts, "mhit", seek, 4)) + { /* mhit header -> start of tracks*/ + break; + } + zip = get32lint (cts, seek+4); + CHECK_ERROR (fimp, FALSE); + if (zip == 0) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: hunk length 0 for hunk at %ld in file '%s'."), + seek, cts->filename); + return FALSE; + } + seek += zip; + } + /* now we should be at the first MHIT */ + + /* get every file entry */ + for (i=0; i<nr_tracks; ++i) + { + seek = get_mhit (fimp, seek); + if (fimp->error) return FALSE; + if (seek == -1) + { /* this should not be -- issue warning */ + g_warning (_("iTunesDB possibly corrupt: number of tracks (mhit hunks) inconsistent. Trying to continue.\n")); + break; + } + } + + /* next: playlists */ + seek = pl_mhsd; + for (;;) + { /* this is all a bit of magic to make sure we can handle + slightly "off-standard" iTunesDBs as well. Normally we + would expect hunks in the following order: <mhsd type 2>, + <mhlp> containing the number of playlists, <mhyp>: first + playlist header. Here we just scan everything until we find + the first <mhyp> ignoring everything we don't know. */ + zip = get32lint (cts, seek+4); + if (zip == 0) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: hunk length 0 for hunk at %ld in file '%s'."), + seek, cts->filename); + return FALSE; + } + CHECK_ERROR (fimp, FALSE); + if (cmp_n_bytes_seek (cts, "mhsd", seek, 4)) + { /* We just check if it's actually a playlist mhsd (type=2) + or not (type = 1, should not be...) */ + guint32 type; + guint32 len = get32lint (cts, seek+8); + CHECK_ERROR (fimp, FALSE); + type = get32lint (cts, seek+12); + CHECK_ERROR (fimp, FALSE); + if (type != 2) + { /* this is not a playlist MHSD -> skip it */ + seek += len; + continue; + } + else + { /* jump to next hunk */ + seek += zip; + continue; + } + } + else + { + CHECK_ERROR (fimp, FALSE); + } + if (cmp_n_bytes_seek (cts, "mhlp", seek, 4)) + { /* mhlp header -> number of playlists */ + nr_playlists = get32lint (cts, seek+8); + CHECK_ERROR (fimp, FALSE); + seek += zip; + continue; + } + else + { + CHECK_ERROR (fimp, FALSE); + } + if (cmp_n_bytes_seek (cts, "mhyp", seek, 4)) + { /* mhyp header -> start of playlists */ + break; + } + else + { + CHECK_ERROR (fimp, FALSE); + } + seek += zip; + } + +#if ITUNESDB_DEBUG + fprintf(stderr, "iTunesDB part2 starts at: %x\n", (int)seek); +#endif + + /* Create track-id tree for quicker track lookup */ + fimp->idtree = itdb_track_id_tree_create (fimp->itdb); + for (i=0; i<nr_playlists; ++i) + { + seek = get_playlist (fimp, seek); + if (fimp->error) return FALSE; + if (seek == -1) + { /* this should not be -- issue warning */ + g_warning (_("iTunesDB possibly corrupt: number of playlists (mhyp hunks) inconsistent. Trying to continue.\n")); + break; + } + } + itdb_track_id_tree_destroy (fimp->idtree); + fimp->idtree = NULL; + + return TRUE; +} + + +/* Parse the Itdb_iTunesDB. + Returns a pointer to the Itdb_iTunesDB struct holding the tracks and the + playlists. + "mp" should point to the mount point of the iPod, + e.g. "/mnt/ipod" and be in local encoding */ +Itdb_iTunesDB *itdb_parse (const gchar *mp, GError **error) +{ + gchar *filename; + Itdb_iTunesDB *itdb = NULL; + const gchar *db[] = {"iPod_Control","iTunes","iTunesDB",NULL}; + + filename = itdb_resolve_path (mp, db); + if (filename) + { + itdb = itdb_parse_file (filename, error); + if (itdb) + { + itdb->mountpoint = g_strdup (mp); + } + g_free (filename); + } + else + { + gchar *str = g_build_filename (mp, db[0], db[1], db[2], db[3], NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("File not found: '%s'."), + str); + g_free (str); + } + return itdb; +} + + +/* Same as itunesdb_parse(), but filename is specified directly. */ +Itdb_iTunesDB *itdb_parse_file (const gchar *filename, GError **error) +{ + FImport *fimp; + Itdb_iTunesDB *itdb; + gboolean success = FALSE; + + g_return_val_if_fail (filename, NULL); + + fimp = g_new0 (FImport, 1); + itdb = itdb_new (); + itdb->filename = g_strdup (filename); + fimp->itdb = itdb; + + fimp->itunesdb = fcontents_read (filename, error); + + if (fimp->itunesdb) + { + if (playcounts_init (fimp)) + { + if (parse_fimp (fimp)) + { + if (read_OTG_playlists (fimp)) + { + success = TRUE; + } + } + } + } + + if (!success) + { + itdb_free (itdb); + itdb = NULL; + if (fimp->error) + g_propagate_error (error, fimp->error); + } + itdb_free_fimp (fimp); + return itdb; +} + + +/* up to here we had the functions for reading the iTunesDB */ +/* ---------------------------------------------------------------------- */ +/* from here on we have the functions for writing the iTunesDB */ + +/* will expand @cts when necessary in order to accomodate @len bytes + starting at @seek */ +static void wcontents_maybe_expand (WContents *cts, gulong len, + gulong seek) +{ + g_return_if_fail (cts); + + while (cts->pos+len > cts->total) + { + cts->total += WCONTENTS_STEPSIZE; + cts->contents = g_realloc (cts->contents, cts->total); + } +} + + +/* Write @data, @n bytes long to position @seek. Will always be + * successful because glib terminates when out of memory */ +static void put_data_seek (WContents *cts, gchar *data, + gulong len, gulong seek) +{ + g_return_if_fail (cts); + g_return_if_fail (data); + + if (len != 0) + { + wcontents_maybe_expand (cts, len, seek); + + memcpy (&cts->contents[seek], data, len); + /* adjust end position if necessary */ + if (seek+len > cts->pos) + cts->pos = seek+len; + } +} + + + +/* Write @data, @n bytes long to end of @cts. Will always be + * successful because glib terminates when out of memory */ +static void put_data (WContents *cts, gchar *data, gulong len) +{ + g_return_if_fail (cts); + + put_data_seek (cts, data, len, cts->pos); +} + + +/* Write 1-byte integer @n to @cts */ +static void put8int (WContents *cts, guint8 n) +{ + put_data (cts, (gchar *)&n, 1); +} + + +/* Write 2-byte integer @n to @cts in little endian order. */ +static void put16lint (WContents *cts, guint16 n) +{ +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + n = GUINT16_SWAP_LE_BE (n); +# endif + put_data (cts, (gchar *)&n, 2); +} + + +/* Write 4-byte integer @n to @cts in little endian order. */ +static void put32lint (WContents *cts, guint32 n) +{ +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + n = GUINT32_SWAP_LE_BE (n); +# endif + put_data (cts, (gchar *)&n, 4); +} + + +/* Append @n times 2-byte-long zeros */ +static void put16_n0 (WContents *cts, gulong n) +{ + g_return_if_fail (cts); + + if (n>0) + { + wcontents_maybe_expand (cts, 2*n, cts->pos); + memset (&cts->contents[cts->pos], 0, 2*n); + cts->pos += 2*n; + } +} + +/* Write 3-byte integer @n to @cts in big endian order. */ +static void put24bint (WContents *cts, guint32 n) +{ + gchar buf[3] ; + buf[0] = (n >> 16) & 0xff ; + buf[1] = (n >> 8) & 0xff ; + buf[2] = (n >> 0) & 0xff ; + put_data (cts, buf, 3); +} + + +/* Write 4-byte integer @n to @cts at position @seek in little + endian order. */ +static void put32lint_seek (WContents *cts, guint32 n, gulong seek) +{ +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + n = GUINT32_SWAP_LE_BE (n); +# endif + put_data_seek (cts, (gchar *)&n, 4, seek); +} + + +/* Write 8-byte integer @n to @cts in big little order. */ +static void put64lint (WContents *cts, guint64 n) +{ +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + n = GUINT64_SWAP_LE_BE (n); +# endif + put_data (cts, (gchar *)&n, 8); +} + + +/* Write 4-byte integer @n to @cts in big endian order. */ +static void put32bint (WContents *cts, guint32 n) +{ +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + n = GUINT32_SWAP_LE_BE (n); +# endif + put_data (cts, (gchar *)&n, 4); +} + + +/* Write 8-byte integer @n to cts in big endian order. */ +static void put64bint (WContents *cts, guint64 n) +{ +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + n = GUINT64_SWAP_LE_BE (n); +# endif + put_data (cts, (gchar *)&n, 8); +} + + +#if 0 +/* Write 4-byte integer @n to @cts at position @seek in big endian + order. */ +static void put32bint_seek (WContents *cts, guint32 n, gulong seek) +{ +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + n = GUINT32_SWAP_LE_BE (n); +# endif + put_data_seek (cts, (gchar *)&n, 4, seek); +} + +/* Write 8-byte integer @n to @cts at position @seek in big endian + order. */ +static void put64bint_seek (WContents *cts, guint64 n, gulong seek) +{ +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + n = GUINT64_SWAP_LE_BE (n); +# endif + put_data_seek (cts, (gchar *)&n, 8, seek); +} +#endif + + +/* Append @n times 4-byte-long zeros */ +static void put32_n0 (WContents *cts, gulong n) +{ + g_return_if_fail (cts); + + if (n>0) + { + wcontents_maybe_expand (cts, 4*n, cts->pos); + memset (&cts->contents[cts->pos], 0, 4*n); + cts->pos += 4*n; + } +} + + + +/* Write out the mhbd header. Size will be written later */ +static void mk_mhbd (FExport *fexp) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itdb); + g_return_if_fail (fexp->itunesdb); + + cts = fexp->itunesdb; + + put_data (cts, "mhbd", 4); + put32lint (cts, 104); /* header size */ + put32lint (cts, -1); /* size of whole mhdb -- fill in later */ + put32lint (cts, 1); /* ? */ + if (fexp->itdb->version < 0x09) fexp->itdb->version = 0x09; + /* Version number: 0x01: iTunes 2 + 0x02: iTunes 3 + 0x09: iTunes 4.2 + 0x0a: iTunes 4.5 + 0x0b: iTunes 4.7 + 0x0c: iTunes 4.71/4.8 (required for shuffle) + 0x0d: iTunes 4.9 */ + fexp->itdb->version = 0x0d; + put32lint (cts, fexp->itdb->version); + put32lint (cts, 2); /* 2 children (track list and playlist list) */ + put64lint (cts, fexp->itdb->id); + put32lint (cts, 2); /* ? */ + put32_n0 (cts, 17); /* dummy space */ +} + +/* Fill in the length of a standard header */ +static void fix_header (WContents *cts, gulong header_seek) +{ + put32lint_seek (cts, cts->pos-header_seek, header_seek+8); +} + + +/* Write out the mhsd header. Size will be written later */ +static void mk_mhsd (FExport *fexp, guint32 type) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itdb); + g_return_if_fail (fexp->itunesdb); + + cts = fexp->itunesdb; + + put_data (cts, "mhsd", 4); + put32lint (cts, 96); /* Headersize */ + put32lint (cts, -1); /* size of whole mhsd -- fill in later */ + put32lint (cts, type); /* type: 1 = track, 2 = playlist */ + put32_n0 (cts, 20); /* dummy space */ +} + + +/* Write out the mhlt header. */ +static void mk_mhlt (FExport *fexp, guint32 num) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itdb); + g_return_if_fail (fexp->itunesdb); + + cts = fexp->itunesdb; + + put_data (cts, "mhlt", 4); + put32lint (cts, 92); /* Headersize */ + put32lint (cts, num); /* tracks in this itunesdb */ + put32_n0 (cts, 20); /* dummy space */ +} + + +/* Write out the mhit header. Size will be written later */ +static void mk_mhit (WContents *cts, Itdb_Track *track) +{ + g_return_if_fail (cts); + g_return_if_fail (track); + + put_data (cts, "mhit", 4); + put32lint (cts, 156); /* header size */ + put32lint (cts, -1); /* size of whole mhit -- fill in later */ + put32lint (cts, -1); /* nr of mhods in this mhit -- later */ + put32lint (cts, track->id); /* track index number + * */ + put32lint (cts, track->unk020); + put32lint (cts, track->unk024); + /* rating, compil., type */ + put32lint (cts, ((guint32)track->rating << 24) | + ((guint32)track->compilation << 16) | + ((guint32)track->type & 0x0000ffff)); + + put32lint (cts, track->time_added); /* timestamp */ + put32lint (cts, track->size); /* filesize */ + put32lint (cts, track->tracklen); /* length of track in ms */ + put32lint (cts, track->track_nr);/* track number */ + put32lint (cts, track->tracks); /* number of tracks */ + put32lint (cts, track->year); /* the year */ + put32lint (cts, track->bitrate); /* bitrate */ + put32lint (cts, track->samplerate << 16); + put32lint (cts, track->volume); /* volume adjust */ + put32lint (cts, track->starttime); + put32lint (cts, track->stoptime); + put32lint (cts, track->soundcheck); + put32lint (cts, track->playcount);/* playcount */ + put32lint (cts, track->unk084); + put32lint (cts, track->time_played); /* last time played */ + put32lint (cts, track->cd_nr); /* CD number */ + put32lint (cts, track->cds); /* number of CDs */ + put32lint (cts, track->unk100); + put32lint (cts, track->time_modified); /* timestamp */ + put32lint (cts, track->bookmark_time); + put64lint (cts, track->dbid); + if (track->checked) put8int (cts, 1); + else put8int (cts, 0); + put8int (cts, track->app_rating); + put16lint (cts, track->BPM); + put32lint (cts, track->unk124); + put32lint (cts, track->unk128); + put32lint (cts, track->unk132); + put32lint (cts, track->unk136); + put32lint (cts, track->unk140); + put32lint (cts, track->unk144); + put32lint (cts, track->unk148); + put32lint (cts, track->unk152); +} + + +/* Fill in the missing items of the mhit header: + total size and number of mhods */ +static void fix_mhit (WContents *cts, gulong mhit_seek, guint32 mhod_num) +{ + g_return_if_fail (cts); + + /* size of whole mhit */ + put32lint_seek (cts, cts->pos-mhit_seek, mhit_seek+8); + /* nr of mhods */ + put32lint_seek (cts, mhod_num, mhit_seek+12); +} + + +/* Write out one mhod header. + type: see enum of MHMOD_IDs; + data: utf8 string for text items + position indicator for MHOD_ID_PLAYLIST + SPLPref for MHOD_ID_SPLPREF + SPLRules for MHOD_ID_SPLRULES */ +static void mk_mhod (WContents *cts, enum MHOD_ID type, void *data) +{ + g_return_if_fail (cts); + + switch (type) + { + case MHOD_ID_TITLE: + case MHOD_ID_PATH: + case MHOD_ID_ALBUM: + case MHOD_ID_ARTIST: + case MHOD_ID_GENRE: + case MHOD_ID_FDESC: + case MHOD_ID_COMMENT: + case MHOD_ID_COMPOSER: + case MHOD_ID_GROUPING: + g_return_if_fail (data); + { + /* convert to utf16 */ + gunichar2 *entry_utf16 = g_utf8_to_utf16 ((gchar *)data, -1, + NULL, NULL, NULL); + guint32 len = utf16_strlen (entry_utf16); + fixup_little_utf16 (entry_utf16); + put_data (cts, "mhod", 4); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, 2*len+40); /* size of header + body */ + put32lint (cts, type); /* type of the entry */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + put32lint (cts, 1); /* always 1 for these MHOD_IDs*/ + put32lint (cts, 2*len); /* size of string */ + put32_n0 (cts, 2); /* unknown */ + put_data (cts, (gchar *)entry_utf16, 2*len); /* the string */ + g_free (entry_utf16); + } + break; + case MHOD_ID_PLAYLIST: + put_data (cts, "mhod", 4); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, 44); /* size of header + body */ + put32lint (cts, type); /* type of the entry */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + put32lint (cts, (guint32)data);/* position of track in playlist */ + put32_n0 (cts, 4); /* unknown */ + break; + case MHOD_ID_SPLPREF: + g_return_if_fail (data); + { + SPLPref *splp = data; + put_data (cts, "mhod", 4); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, 96); /* size of header + body */ + put32lint (cts, type); /* type of the entry */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + put8int (cts, splp->liveupdate); + put8int (cts, splp->checkrules? 1:0); + put8int (cts, splp->checklimits); + put8int (cts, splp->limittype); + put8int (cts, splp->limitsort & 0xff); + put8int (cts, 0); /* unknown */ + put8int (cts, 0); /* unknown */ + put8int (cts, 0); /* unknown */ + put32lint (cts, splp->limitvalue); + put8int (cts, splp->matchcheckedonly); + /* for the following see note at definitions of limitsort + types in itunesdb.h */ + put8int (cts, (splp->limitsort & 0x80000000) ? 1:0); + put8int (cts, 0); /* unknown */ + put8int (cts, 0); /* unknown */ + put32_n0 (cts, 14); /* unknown */ + } + break; + case MHOD_ID_SPLRULES: + g_return_if_fail (data); + { + SPLRules *splrs = data; + gulong header_seek = cts->pos; /* needed to fix length */ + GList *gl; + gint numrules = g_list_length (splrs->rules); + + put_data (cts, "mhod", 4); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, -1); /* total length, fix later */ + put32lint (cts, type); /* type of the entry */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + /* For some reason this is the only part of the iTunesDB + that uses big endian */ + put_data (cts, "SLst", 4); /* header */ + put32bint (cts, splrs->unk004); /* unknown */ + put32bint (cts, numrules); + put32bint (cts, splrs->match_operator); + put32_n0 (cts, 30); /* unknown */ + /* end of header, now follow the rules */ + for (gl=splrs->rules; gl; gl=gl->next) + { + SPLRule *splr = gl->data; + gint ft; + g_return_if_fail (splr); + ft = itdb_splr_get_field_type (splr); +/* printf ("%p: field: %d ft: %d\n", splr, splr->field, ft);*/ + itdb_splr_validate (splr); + put32bint (cts, splr->field); + put32bint (cts, splr->action); + put32_n0 (cts, 11); /* unknown */ + if (ft == splft_string) + { /* write string-type rule */ + gunichar2 *entry_utf16 = + g_utf8_to_utf16 (splr->string, -1,NULL,NULL,NULL); + gint len = utf16_strlen (entry_utf16); + fixup_big_utf16 (entry_utf16); + put32bint (cts, 2*len); /* length of string */ + put_data (cts, (gchar *)entry_utf16, 2*len); + g_free (entry_utf16); + } + else + { /* write non-string-type rule */ + put32bint (cts, 0x44); /* length of data */ + /* data */ + put64bint (cts, splr->fromvalue); + put64bint (cts, splr->fromdate); + put64bint (cts, splr->fromunits); + put64bint (cts, splr->tovalue); + put64bint (cts, splr->todate); + put64bint (cts, splr->tounits); + put32bint (cts, splr->unk052); + put32bint (cts, splr->unk056); + put32bint (cts, splr->unk060); + put32bint (cts, splr->unk064); + put32bint (cts, splr->unk068); + } + } + /* insert length of mhod junk */ + fix_header (cts, header_seek); + } + break; + case MHOD_ID_MHYP: + g_warning (_("Cannot write mhod of type %d\n"), type); + break; + } +} + + +/* Write out the mhlp header. Size will be written later */ +static void mk_mhlp (FExport *fexp) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itunesdb); + + cts = fexp->itunesdb; + + put_data (cts, "mhlp", 4); /* header */ + put32lint (cts, 92); /* size of header */ + /* playlists on iPod (including main!) */ + put32lint (cts, g_list_length (fexp->itdb->playlists)); + put32_n0 (cts, 20); /* dummy space */ +} + + +/* Write out the long MHOD_ID_PLAYLIST mhod header. + This seems to be an itunespref thing.. dunno know this + but if we set everything to 0, itunes doesn't show any data + even if you drag an mp3 to your ipod: nothing is shown, but itunes + will copy the file! + .. so we create a hardcoded-pref.. this will change in future + Seems to be a Preferences mhod, every PL has such a thing + FIXME !!! */ +static void mk_long_mhod_id_playlist (FExport *fexp, Itdb_Playlist *pl) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itunesdb); + g_return_if_fail (pl); + + cts = fexp->itunesdb; + + put_data (cts, "mhod", 4); /* header */ + put32lint (cts, 0x18); /* size of header ? */ + put32lint (cts, 0x0288); /* size of header + body */ + put32lint (cts, MHOD_ID_PLAYLIST); /* type of the entry */ + put32_n0 (cts, 6); + put32lint (cts, 0x010084); /* ? */ + put32lint (cts, 0x05); /* ? */ + put32lint (cts, 0x09); /* ? */ + put32lint (cts, 0x03); /* ? */ + put32lint (cts, 0x120001); /* ? */ + put32_n0 (cts, 3); + put32lint (cts, 0xc80002); /* ? */ + put32_n0 (cts, 3); + put32lint (cts, 0x3c000d); /* ? */ + put32_n0 (cts, 3); + put32lint (cts, 0x7d0004); /* ? */ + put32_n0 (cts, 3); + put32lint (cts, 0x7d0003); /* ? */ + put32_n0 (cts, 3); + put32lint (cts, 0x640008); /* ? */ + put32_n0 (cts, 3); + put32lint (cts, 0x640017); /* ? */ + put32lint (cts, 0x01); /* bool? (visible? / colums?) */ + put32_n0 (cts, 2); + put32lint (cts, 0x500014); /* ? */ + put32lint (cts, 0x01); /* bool? (visible?) */ + put32_n0 (cts, 2); + put32lint (cts, 0x7d0015); /* ? */ + put32lint (cts, 0x01); /* bool? (visible?) */ + put32_n0 (cts, 114); +} + + + +/* Header for new PL item */ +/* @pos: position in playlist */ +static void mk_mhip (FExport *fexp, guint32 pos, guint32 id) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itunesdb); + + cts = fexp->itunesdb; + + put_data (cts, "mhip", 4); + put32lint (cts, 76); /* 4 */ + put32lint (cts, -1); /* fill in later */ /* 8 */ + put32lint (cts, 1); /* number of children */ /* 12 */ + put32lint (cts, 0); /* unknown */ /* 16 */ + put32lint (cts, 0); /* unknown */ /* 20 */ + put32lint (cts, id); /* id */ /* 24 */ + put32_n0 (cts, 12); /* 28 */ +} + + +/* Write first mhsd hunk. Return FALSE in case of error and set + * fexp->error */ +static gboolean write_mhsd_one(FExport *fexp) +{ + GList *gl; + gulong mhsd_seek; + WContents *cts; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->itunesdb, FALSE); + + cts = fexp->itunesdb; + + mhsd_seek = cts->pos; /* get position of mhsd header */ + mk_mhsd (fexp, 1); /* write header: type 1: tracks */ + /* write header with nr. of tracks */ + mk_mhlt (fexp, g_list_length (fexp->itdb->tracks)); + for (gl=fexp->itdb->tracks; gl; gl=gl->next) /* Write each track */ + { + Itdb_Track *track = gl->data; + guint32 mhod_num = 0; + gulong mhit_seek = cts->pos; + if (!track) + { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Database in memory corrupt (track pointer == NULL). Aborting export.")); + return FALSE; + } + mk_mhit (cts, track); + if (track->title && *track->title) + { + mk_mhod (cts, MHOD_ID_TITLE, track->title); + ++mhod_num; + } + if (track->ipod_path && *track->ipod_path) + { + mk_mhod (cts, MHOD_ID_PATH, track->ipod_path); + ++mhod_num; + } + if (track->album && *track->album) + { + mk_mhod (cts, MHOD_ID_ALBUM, track->album); + ++mhod_num; + } + if (track->artist && *track->artist) + { + mk_mhod (cts, MHOD_ID_ARTIST, track->artist); + ++mhod_num; + } + if (track->genre && *track->genre) + { + mk_mhod (cts, MHOD_ID_GENRE, track->genre); + ++mhod_num; + } + if (track->fdesc && *track->fdesc) + { + mk_mhod (cts, MHOD_ID_FDESC, track->fdesc); + ++mhod_num; + } + if (track->comment && *track->comment) + { + mk_mhod (cts, MHOD_ID_COMMENT, track->comment); + ++mhod_num; + } + if (track->composer && *track->composer) + { + mk_mhod (cts, MHOD_ID_COMPOSER, track->composer); + ++mhod_num; + } + if (track->grouping && *track->grouping) + { + mk_mhod (cts, MHOD_ID_GROUPING, track->grouping); + ++mhod_num; + } + /* Fill in the missing items of the mhit header */ + fix_mhit (cts, mhit_seek, mhod_num); + } + fix_header (cts, mhsd_seek); + return TRUE; +} + +/* corresponds to mk_mhyp */ +/* Return FALSE in case of error and set fexp->error */ +static gboolean write_playlist(FExport *fexp, Itdb_Playlist *pl) +{ + GList *gl; + gulong mhyp_seek, mhip_seek; + guint32 i; + WContents *cts; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->itunesdb, FALSE); + g_return_val_if_fail (pl, FALSE); + + cts = fexp->itunesdb; + mhyp_seek = cts->pos; + +#if ITUNESDB_DEBUG + fprintf(stderr, "Playlist: %s (%d tracks)\n", pl->name, g_list_length (pl->members)); +#endif + + put_data (cts, "mhyp", 4); /* header */ + put32lint (cts, 108); /* length */ + put32lint (cts, -1); /* size -> later */ + if (pl->is_spl) + put32lint (cts, 4); /* nr of mhods */ + else + put32lint (cts, 2); /* nr of mhods */ + /* number of tracks in plist */ + put32lint (cts, g_list_length (pl->members)); + put32lint (cts, pl->type); /* 1 = main, 0 = visible */ + put32lint (cts, 0); /* some timestamp */ + put64lint (cts, pl->id); /* 64 bit ID */ + put32lint (cts, pl->unk036); + put32lint (cts, pl->unk040); + put32lint (cts, pl->unk044); + put32_n0 (cts, 15); /* ? */ + + mk_mhod (cts, MHOD_ID_TITLE, pl->name); + mk_long_mhod_id_playlist (fexp, pl); + + if (pl->is_spl) + { /* write the smart rules */ + mk_mhod (cts, MHOD_ID_SPLPREF, &pl->splpref); + mk_mhod (cts, MHOD_ID_SPLRULES, &pl->splrules); + } + + /* write hard-coded tracks */ + i=0; + for (gl=pl->members; gl; gl=gl->next) + { + Itdb_Track *track = gl->data; + if (!track) + { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Database in memory corrupt (track pointer == NULL). Aborting export.")); + return FALSE; + } + mhip_seek = cts->pos; + mk_mhip (fexp, i, track->id); + mk_mhod (cts, MHOD_ID_PLAYLIST, (void *)i); + /* note: with iTunes 4.9 the mhod is counted as a child to + mhip, so we fill have put the total length of the mhip and + mhod into the mhip header */ + fix_header (cts, mhip_seek); + ++i; + } + fix_header (cts, mhyp_seek); + return TRUE; +} + + + +/* Expects the master playlist to be the first in the list */ +/* Return FALSE in case of error and set fexp->error */ +static gboolean write_mhsd_two(FExport *fexp) +{ + GList *gl; + glong mhsd_seek; + WContents *cts; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->itunesdb, FALSE); + + cts = fexp->itunesdb; + mhsd_seek = cts->pos; /* get position of mhsd header */ + mk_mhsd (fexp, 2); /* write header: type 2: playlists */ + /* write header with nr. of playlists */ + mk_mhlp (fexp); + for(gl=fexp->itdb->playlists; gl; gl=gl->next) + { + Itdb_Playlist *pl = gl->data; + if (!pl) + { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Database in memory corrupt (playlist pointer == NULL). Aborting export.")); + return FALSE; + } + write_playlist (fexp, pl); + if (fexp->error) return FALSE; + } + fix_header (cts, mhsd_seek); + return TRUE; +} + + +/* create a WContents structure */ +static WContents *wcontents_new (const gchar *filename) +{ + WContents *cts; + + g_return_val_if_fail (filename, NULL); + + cts = g_new0 (WContents, 1); + cts->filename = g_strdup (filename); + + return cts; +} + + +/* write the contents of WContents. Return FALSE on error and set + * cts->error accordingly. */ +static gboolean wcontents_write (WContents *cts) +{ + int fd; + + g_return_val_if_fail (cts, FALSE); + g_return_val_if_fail (cts->filename, FALSE); + + fd = creat (cts->filename, S_IRWXU|S_IRWXG|S_IRWXO); + + if (fd == -1) + { + cts->error = g_error_new (G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Opening of '%s' for writing failed."), + cts->filename); + return FALSE; + } + if (cts->contents && cts->pos) + { + ssize_t written = write (fd, cts->contents, cts->pos); + if (written == -1) + { + cts->error = g_error_new (G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Writing to '%s' failed."), + cts->filename); + close (fd); + return FALSE; + } + } + fd = close (fd); + if (fd == -1) + { + cts->error = g_error_new (G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Writing to '%s' failed (%s)."), + cts->filename, g_strerror (errno)); + return FALSE; + } + return TRUE; +} + + +/* Free memory associated with WContents @cts */ +static void wcontents_free (WContents *cts) +{ + if (cts) + { + g_free (cts->filename); + g_free (cts->contents); + /* must not g_error_free (cts->error) because the error was + propagated -> might free the error twice */ + g_free (cts); + } +} + + +/* reassign the iPod IDs and make sure the itdb->tracks are in the + same order as the mpl */ +static void reassign_ids (Itdb_iTunesDB *itdb) +{ + guint32 id = 52; + GList *gl; + Itdb_Playlist *mpl; + + g_return_if_fail (itdb); + + /* copy mpl->members to itdb->tracks to make sure they are in the + same order (otherwise On-The-Go Playlists will not show the + correct content) */ + mpl = itdb_playlist_mpl (itdb); + g_return_if_fail (mpl); + g_return_if_fail (g_list_length (mpl->members) == g_list_length (itdb->tracks)); + g_list_free (itdb->tracks); + itdb->tracks = g_list_copy (mpl->members); + + /* assign unique IDs */ + for (gl=itdb->tracks; gl; gl=gl->next) + { + Itdb_Track *track = gl->data; + g_return_if_fail (track); + track->id = id++; + } +} + + + +/* Do the actual writing to the iTunesDB */ +/* If @filename==NULL, itdb->filename is tried */ +gboolean itdb_write_file (Itdb_iTunesDB *itdb, const gchar *filename, + GError **error) +{ + FExport *fexp; + gulong mhbd_seek = 0; + WContents *cts; + gboolean result = TRUE;; + + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (filename || itdb->filename, FALSE); + + if (!filename) filename = itdb->filename; + + reassign_ids (itdb); + + fexp = g_new0 (FExport, 1); + fexp->itdb = itdb; + fexp->itunesdb = wcontents_new (filename); + cts = fexp->itunesdb; + + mk_mhbd (fexp); + if (write_mhsd_one(fexp)) + { /* write playlists mhsd */ + if (write_mhsd_two(fexp)) + { + fix_header (cts, mhbd_seek); + } + } + if (!fexp->error) + { + if (!wcontents_write (cts)) + g_propagate_error (&fexp->error, cts->error); + } + if (fexp->error) + { + g_propagate_error (error, fexp->error); + result = FALSE; + } + wcontents_free (cts); + g_free (fexp); + if (result == TRUE) + { + gchar *fn = g_strdup (filename); + g_free (itdb->filename); + itdb->filename = fn; + } + /* make sure all buffers are flushed as some people tend to + disconnect as soon as gtkpod returns */ + sync (); + + return result; +} + +/* Write out an iTunesDB. + + First reassigns unique IDs to all tracks. + + An existing "Play Counts" file is renamed to "Play Counts.bak" if + the export was successful. + + An existing "OTGPlaylistInfo" file is removed if the export was + successful. + + Returns TRUE on success, FALSE on error, in which case @error is + set accordingly. + + @mp must point to the mount point of the iPod, e.g. "/mnt/ipod" and + be in local encoding. If mp==NULL, itdb->mountpoint is tried. */ +gboolean itdb_write (Itdb_iTunesDB *itdb, const gchar *mp, GError **error) +{ + gchar *itunes_filename, *itunes_path; + const gchar *db[] = {"iPod_Control","iTunes",NULL}; + gboolean result = FALSE; + + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (mp || itdb->mountpoint, FALSE); + + if (!mp) mp = itdb->mountpoint; + + itunes_path = itdb_resolve_path (mp, db); + + if(!itunes_path) + { + gchar *str = g_build_filename (mp, db[0], db[1], db[2], NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Path not found: '%s'."), + str); + g_free (str); + return FALSE; + } + + itunes_filename = g_build_filename (itunes_path, "iTunesDB", NULL); + + result = itdb_write_file (itdb, itunes_filename, error); + + g_free(itunes_filename); + g_free(itunes_path); + + if (result == TRUE) + result = itdb_rename_files (mp, error); + + if (result == TRUE) + { + gchar *mnp = g_strdup (mp); + g_free (itdb->mountpoint); + itdb->mountpoint = mnp; + } + + /* make sure all buffers are flushed as some people tend to + disconnect as soon as gtkpod returns */ + sync (); + + return result; +} + + +/* from here on we have the functions for writing the iTunesDB */ +/* -------------------------------------------------------------------- */ +/* up to here we had the functions for writing the iTunesSD */ + +/* +| Copyright (C) 2005 Jorg Schuler <jcsjcs at users.sourceforge.net> +| Part of the gtkpod project. +| +| Based on itunessd.c written by Steve Wahl for gtkpod-0.88: +| +| Copyright 2005 Steve Wahl <steve at pro-ns dot net> +| +| This file contains routines to create the iTunesSD file, as +| used by the ipod shuffle. +| +| Like itunesdb.c, it is derived from the perl script "mktunes.pl" +| (part of the gnupod-tools collection) written by Adrian +| Ulrich <pab at blinkenlights.ch>. +| +| Small(?) portions derived from itunesdb.c, so Jorg Schuler probably +| has some copyright ownership in this file as well. +| +| 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! +| +*/ + +/* notes: + + All software currently seems to write iTunesDB as well as iTunesSD + on the iPod shuffle. I assume that reading from the iTunesSD file + is not necessary. The iTunesStats file is different, but I leave + that for another day. + + The iTunesSD file format is as follows (taken from WikiPodLinux, feb + '05): + + Offset Field Bytes Value + (hex) (dec) + + iTunesSD header (occurs once, at beginning of file): + + 00 num_songs 3 number of song entries in the file + + 03 unknown 3 always(?) 0x010600 + 06 header size 3 size of the header (0x12, 18 bytes) + 09 unknown 3 possibly zero padding + + iTunesSD song entry format (occurs once for each song) + + 000 size of entry 3 always(?) 0x00022e (558 bytes) + 003 unk1 3 unknown, always(?) 0x5aa501 + 006 starttime 3 Start Time, in 256 ms increments + e.g. 60s = 0xea (234 dec) + 009 unk2 3 unknown (always 0?) + 00C unk3 3 unknown, some relationship to starttime + 00F stoptime 3 Stop Time, also in 256 ms increments. + Zero means play to end of file. + 012 unk4 3 Unknown. + 015 unk5 3 Unknown, but associated with stoptime? + 018 volume 3 Volume - ranges from 0x00 (-100%) to 0x64 + (0%) to 0xc8 (100%) + 01B file_type 3 0x01 = MP3, 0x02 = AAC, 0x04=WAV + 01E unk6 3 unknown (always 0x200?) + 021 filename 522 filename of the song, padded at the end + with 0's. Note: forward slashes are used + here, not colons like in the iTunesDB -- + for example, + "/iPod_Control/Music/F00/Song.mp3" + 22B shuffleflag 1 If this value is 0x00, the song will be + skipped while the player is in shuffle + mode. Any other value will allow it to be + played in both normal and shuffle modes. + iTunes 4.7.1 sets this to 0 for audio books. + 22C bookmarkflag 1 If this flag is 0x00, the song will not be + bookmarkable (i.e. its playback position + won't be saved when switching to a different + song). Any other value wil make it + Bookmarkable. Unlike hard drive based iPods, + all songs can be marked as bookmarkable, + not just .m4b and .aa + 22D unknownflag 1 unknown, always? 0x00. + +All integers in the iTunesSD file are in BIG endian form... + +*/ + + +/* Write out an iTunesSD for the Shuffle. + + First reassigns unique IDs to all tracks. + + An existing "Play Counts" file is renamed to "Play Counts.bak" if + the export was successful. + + An existing "OTGPlaylistInfo" file is removed if the export was + successful. + + Returns TRUE on success, FALSE on error, in which case @error is + set accordingly. + + @mp must point to the mount point of the iPod, e.g. "/mnt/ipod" and + be in local encoding. If mp==NULL, itdb->mountpoint is tried. */ + + +gboolean itdb_shuffle_write (Itdb_iTunesDB *itdb, + const gchar *mp, GError **error) +{ + gchar *itunes_filename, *itunes_path; + const gchar *db[] = {"iPod_Control","iTunes",NULL}; + gboolean result = FALSE; + + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (mp || itdb->mountpoint, FALSE); + + if (!mp) mp = itdb->mountpoint; + + itunes_path = itdb_resolve_path (mp, db); + + if(!itunes_path) + { + gchar *str = g_build_filename (mp, db[0], db[1], db[2], NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Path not found: '%s'."), + str); + g_free (str); + return FALSE; + } + + itunes_filename = g_build_filename (itunes_path, "iTunesSD", NULL); + + result = itdb_shuffle_write_file (itdb, itunes_filename, error); + + g_free(itunes_filename); + g_free(itunes_path); + + if (result == TRUE) + result = itdb_rename_files (mp, error); + + if (result == TRUE) + { + gchar *mnp = g_strdup (mp); + g_free (itdb->mountpoint); + itdb->mountpoint = mnp; + } + + /* make sure all buffers are flushed as some people tend to + disconnect as soon as gtkpod returns */ + sync (); + + return result; +} + + +/* Do the actual writing to the iTunesSD */ +/* If @filename cannot be NULL */ +gboolean itdb_shuffle_write_file (Itdb_iTunesDB *itdb, + const gchar *filename, GError **error) +{ + gboolean haystack (gchar *fdesc, gchar **desclist) + { + gchar **dlp; + if (!fdesc || !desclist) return FALSE; + for (dlp=desclist; *dlp; ++dlp) + { + if (strstr (fdesc, *dlp)) return TRUE; + } + return FALSE; + } + + FExport *fexp; + GList *gl; + WContents *cts; + gboolean result = TRUE;; + + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (filename, FALSE); + + reassign_ids (itdb); + + fexp = g_new0 (FExport, 1); + fexp->itdb = itdb; + fexp->itunesdb = wcontents_new (filename); + cts = fexp->itunesdb; + + put24bint (cts, itdb_tracks_number (itdb)); + put24bint (cts, 0x010600); + put24bint (cts, 0x12); /* size of header */ + put24bint (cts, 0x0); /* padding? */ + put24bint (cts, 0x0); + put24bint (cts, 0x0); + + for (gl=itdb->tracks; gl; gl=gl->next) + { + Itdb_Track *tr = gl->data; + gchar *path; + gunichar2 *path_utf16; + guint32 pathlen; + gchar *mp3_desc[] = {"MPEG", "MP3", "mpeg", "mp3", NULL}; + gchar *mp4_desc[] = {"AAC", "MP4", "aac", "mp4", NULL}; + gchar *wav_desc[] = {"WAV", "wav", NULL}; + + g_return_val_if_fail (tr, FALSE); + + put24bint (cts, 0x00022e); + put24bint (cts, 0x5aa501); + /* starttime is in 256 ms incr. for shuffle */ + put24bint (cts, tr->starttime / 256); + put24bint (cts, 0); + put24bint (cts, 0); + put24bint (cts, tr->stoptime / 256); + put24bint (cts, 0); + put24bint (cts, 0); + /* track->volume ranges from -255 to +255 */ + /* we want 0 - 200 */ + put24bint (cts, ((tr->volume + 255) * 201) / 511); + + /* The next one should be 0x01 for MP3, + ** 0x02 for AAC, and 0x04 for WAV, but I can't find + ** a suitable indicator within the track structure? */ + /* JCS: let's do heuristic on tr->fdesc which would contain + "MPEG audio file", "AAC audio file", "Protected AAC audio + file", "AAC audio book file", "WAV audio file" (or similar + if not written by gtkpod */ + + if (haystack (tr->fdesc, mp3_desc)) + put24bint (cts, 0x01); + else if (haystack (tr->fdesc, mp4_desc)) + put24bint (cts, 0x02); + else if (haystack (tr->fdesc, wav_desc)) + put24bint (cts, 0x04); + else + put24bint (cts, 0x01); /* default to mp3 */ + + put24bint (cts, 0x200); + + /* shuffle uses forward slash separator, not colon */ + path = g_strdup (tr->ipod_path); + itdb_filename_ipod2fs (path); + path_utf16 = g_utf8_to_utf16 (path, -1, NULL, NULL, NULL); + pathlen = utf16_strlen (path_utf16); + if (pathlen > 261) pathlen = 261; + fixup_little_utf16 (path_utf16); + put_data (cts, (gchar *)path_utf16, 2*pathlen); + /* pad to 522 bytes */ + put16_n0 (cts, 261-pathlen); + g_free(path); + g_free(path_utf16); + + /* XXX FIXME: should depend on something, not hardcoded */ + put8int (cts, 0x1); /* song used in shuffle mode */ + put8int (cts, 0); /* song will not be bookmarkable */ + put8int (cts, 0); + } + if (!fexp->error) + { + if (!wcontents_write (cts)) + g_propagate_error (&fexp->error, cts->error); + } + if (fexp->error) + { + g_propagate_error (error, fexp->error); + result = FALSE; + } + wcontents_free (cts); + g_free (fexp); + + /* make sure all buffers are flushed as some people tend to + disconnect as soon as gtkpod returns */ + sync (); + + return result; +} + + + + + + + + + + + +/*------------------------------------------------------------------*\ + * * + * Other file/filename stuff * + * * +\*------------------------------------------------------------------*/ + + +/* (Renames/removes some files on the iPod (Playcounts, OTG + semaphore). May have to be called if you write the iTunesDB not + directly to the iPod but to some other location and then manually + copy the file from there to the iPod. */ +/* Returns FALSE on error and sets @error accordingly */ +gboolean itdb_rename_files (const gchar *mp, GError **error) +{ + const gchar *db_itd[] = {"iPod_Control", "iTunes", NULL}; + const gchar *db_plc_o[] = {"Play Counts", NULL}; + const gchar *db_otg[] = {"OTGPlaylistInfo", NULL}; + gchar *itunesdir; + gchar *plcname_o; + gchar *plcname_n; + gchar *otgname; + gboolean result = TRUE; + + itunesdir = itdb_resolve_path (mp, db_itd); + if(!itunesdir) + { + gchar *str = g_build_filename (mp, db_itd[0], + db_itd[1], db_itd[2], NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Path not found: '%s'."), + str); + g_free (str); + return FALSE; + } + + + plcname_o = itdb_resolve_path (itunesdir, db_plc_o); + plcname_n = g_build_filename (itunesdir, + "Play Counts.bak", NULL); + otgname = itdb_resolve_path (itunesdir, db_otg); + + /* rename "Play Counts" to "Play Counts.bak" */ + if (plcname_o) + { + if (rename (plcname_o, plcname_n) == -1) + { /* an error occured */ + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error renaming '%s' to '%s' (%s)."), + plcname_o, plcname_n, g_strerror (errno)); + result = FALSE; + } + } + + /* remove "OTGPlaylistInfo" (the iPod will remove the remaining + * files */ + if (otgname) + { + if (unlink (otgname) == -1) + { + if (error && !*error) + { /* don't overwrite previous error */ + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error removing '%s' (%s)."), + otgname, g_strerror (errno)); + } + result = FALSE; + } + } + + g_free (plcname_o); + g_free (plcname_n); + g_free (otgname); + g_free (itunesdir); + + return result; +} + + +/* Convert string from casual PC file name to iPod iTunesDB format + * using ':' instead of slashes + */ +void itdb_filename_fs2ipod (gchar *ipod_file) +{ + g_strdelimit (ipod_file, G_DIR_SEPARATOR_S, ':'); +} + +/* Convert string from iPod iTunesDB format to casual PC file name + * using slashes instead of ':' + */ +void itdb_filename_ipod2fs (gchar *ipod_file) +{ + g_strdelimit (ipod_file, ":", G_DIR_SEPARATOR); +} + + + +/* Copy one track to the iPod. The PC filename is @filename + and is taken literally. + @path is the mountpoint of the iPod (in local encoding). + + If @track->transferred is set to TRUE, nothing is done. Upon + successful transfer @track->transferred is set to TRUE. + + For storage, the directories "f00 ... f19" will be + cycled through. + + The filename is constructed as "gtkpod"<random number> and copied + to @track->ipod_path. If this file already exists, <random number> + is adjusted until an unused filename is found. + + If @track->ipod_path is already set, this one will be used + instead. If a file with this name already exists, it will be + overwritten. */ +gboolean itdb_cp_track_to_ipod (const gchar *mp, Itdb_Track *track, + gchar *filename, GError **error) +{ + static gint dir_num = -1; + gchar *track_db_path, *ipod_fullfile; + gboolean success; + gint mplen = 0; + + g_return_val_if_fail (mp, FALSE); + g_return_val_if_fail (track, FALSE); + g_return_val_if_fail (filename, FALSE); + + if(track->transferred) return TRUE; /* nothing to do */ + + /* If track->ipod_path exists, we use that one instead. */ + ipod_fullfile = itdb_filename_on_ipod (mp, track); + + if (!ipod_fullfile) + { + gchar *dest_components[] = {"iPod_Control", "Music", + NULL, NULL, NULL}; + gchar *parent_dir_filename; + gchar *original_suffix; + gchar dir_num_str[5]; + gint32 oops = 0; + gint32 rand = g_random_int_range (0, 899999); /* 0 to 900000 */ + + if (dir_num == -1) dir_num = g_random_int_range (0, 20); + else dir_num = (dir_num + 1) % 20; + + g_snprintf (dir_num_str, 5, "F%02d", dir_num); + dest_components[2] = dir_num_str; + + parent_dir_filename = + itdb_resolve_path (mp, (const gchar **)dest_components); + + if(parent_dir_filename == NULL) + { + /* Can't find the F%02d directory */ + gchar *str = g_build_filename (mp, dest_components[0], + dest_components[1], + dest_components[2], + dest_components[3], NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Path not found: '%s'."), + str); + g_free (str); + return FALSE; + } + + /* we need the original suffix of pcfile to construct a correct ipod + filename */ + original_suffix = strrchr (filename, '.'); + /* If there is no ".mp3", ".m4a" etc, set original_suffix to empty + string. Note: the iPod will most certainly ignore this file... */ + if (!original_suffix) original_suffix = ""; + + do + { /* we need to loop until we find an unused filename */ + dest_components[3] = + g_strdup_printf("gtkpod%06d%s", + rand + oops, original_suffix); + ipod_fullfile = itdb_resolve_path ( + parent_dir_filename, + (const gchar **)&dest_components[3]); + if(ipod_fullfile) + { /* already exists -- try next */ + g_free(ipod_fullfile); + ipod_fullfile = NULL; + } + else + { /* found unused file -- build filename */ + ipod_fullfile = g_build_filename (parent_dir_filename, + dest_components[3], NULL); + } + g_free (dest_components[3]); + ++oops; + } while (!ipod_fullfile); + g_free(parent_dir_filename); + } + /* now extract filepath for track->ipod_path from ipod_fullfile */ + /* ipod_path must begin with a '/' */ + mplen = strlen (mp); /* length of mountpoint in bytes */ + if (ipod_fullfile[mplen] == G_DIR_SEPARATOR) + { + track_db_path = g_strdup (&ipod_fullfile[mplen]); + } + else + { + track_db_path = g_strdup_printf ("%c%s", G_DIR_SEPARATOR, + &ipod_fullfile[mplen]); + } + /* convert to iPod type */ + itdb_filename_fs2ipod (track_db_path); + +/* printf ("ff: %s\ndb: %s\n", ipod_fullfile, track_db_path); */ + + success = itdb_cp (filename, ipod_fullfile, error); + if (success) + { + track->transferred = TRUE; + g_free (track->ipod_path); + track->ipod_path = g_strdup (track_db_path); + } + + g_free (track_db_path); + g_free (ipod_fullfile); + return success; +} + + +/* Return the full iPod filename as stored in @track. Return value + must be g_free()d after use. + @mp: mount point of the iPod file system (in local encoding) + @track: track + Return value: full filename to @track on the iPod or NULL if no + filename is set in @track. + + NOTE: NULL is returned when the file does not exist. + + NOTE: this code works around a problem on some systems (see + itdb_resolve_path() ) and might return a filename with different + case than the original filename. Don't copy it back to @track + unless you must */ +gchar *itdb_filename_on_ipod (const gchar *mp, Itdb_Track *track) +{ + gchar *result = NULL; + + g_return_val_if_fail (track, NULL); + + if(track->ipod_path && *track->ipod_path) + { + gchar *buf = g_strdup (track->ipod_path); + itdb_filename_ipod2fs (buf); + result = g_build_filename (mp, buf, NULL); + g_free (buf); + if (!g_file_test (result, G_FILE_TEST_EXISTS)) + { + gchar **components = g_strsplit (track->ipod_path,":",10); + g_free (result); + result = itdb_resolve_path (mp, (const gchar **)components); + g_strfreev (components); + } + } + return result; +} + + +/* Copy file "from_file" to "to_file". + Returns TRUE on success, FALSE otherwise */ +gboolean itdb_cp (const gchar *from_file, const gchar *to_file, + GError **error) +{ + gchar *data; + glong bread, bwrite; + FILE *file_in = NULL; + FILE *file_out = NULL; + +#if ITUNESDB_DEBUG + fprintf(stderr, "Entered itunesdb_cp: '%s', '%s'\n", from_file, to_file); +#endif + + g_return_val_if_fail (from_file, FALSE); + g_return_val_if_fail (to_file, FALSE); + + data = g_malloc (ITUNESDB_COPYBLK); + + file_in = fopen (from_file, "r"); + if (file_in == NULL) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error opening '%s' for reading (%s)."), + from_file, g_strerror (errno)); + goto err_out; + } + + file_out = fopen (to_file, "w"); + if (file_out == NULL) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error opening '%s' for writing (%s)."), + to_file, g_strerror (errno)); + goto err_out; + } + + do { + bread = fread (data, 1, ITUNESDB_COPYBLK, file_in); +#if ITUNESDB_DEBUG + fprintf(stderr, "itunesdb_cp: read %ld bytes\n", bread); +#endif + if (bread == 0) + { + if (feof (file_in) == 0) + { /* error -- not end of file! */ + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error while reading from '%s' (%s)."), + from_file, g_strerror (errno)); + goto err_out; + } + } + else + { + bwrite = fwrite (data, 1, bread, file_out); +#if ITUNESDB_DEBUG + fprintf(stderr, "itunesdb_cp: wrote %ld bytes\n", bwrite); +#endif + if (bwrite != bread) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error while writing to '%s' (%s)."), + to_file, g_strerror (errno)); + goto err_out; + } + } + } while (bread != 0); + + if (fclose (file_in) != 0) + { + file_in = NULL; + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error when closing '%s' (%s)."), + from_file, g_strerror (errno)); + goto err_out; + } + if (fclose (file_out) != 0) + { + file_out = NULL; + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error when closing '%s' (%s)."), + to_file, g_strerror (errno)); + goto err_out; + } + g_free (data); + return TRUE; + + err_out: + if (file_in) fclose (file_in); + if (file_out) fclose (file_out); + remove (to_file); + g_free (data); + return FALSE; +} + + + + + + +/*------------------------------------------------------------------*\ + * * + * Timestamp stuff * + * * +\*------------------------------------------------------------------*/ + +guint64 itdb_time_get_mac_time (void) +{ + GTimeVal time; + + g_get_current_time (&time); + return itdb_time_host_to_mac (time.tv_sec); +} + + +/* convert Macintosh timestamp to host system time stamp -- modify + * this function if necessary to port to host systems with different + * start of Epoch */ +/* A "0" time will not be converted */ +time_t itdb_time_mac_to_host (guint64 mactime) +{ + if (mactime != 0) return (time_t)(mactime - 2082844800); + else return (time_t)mactime; +} + + +/* convert host system timestamp to Macintosh time stamp -- modify + * this function if necessary to port to host systems with different + * start of Epoch */ +guint64 itdb_time_host_to_mac (time_t time) +{ + return (guint64)(((gint64)time) + 2082844800); +} |