diff --git a/src/itdb_itunesdb.c b/src/itdb_itunesdb.c index a177e26..c789cf4 100644 --- a/src/itdb_itunesdb.c +++ b/src/itdb_itunesdb.c @@ -1156,6 +1156,7 @@ static gboolean playcounts_plist_read (FImport *fimp, GValue *plist_data) GHashTable *pc_dict, *track_dict; GValue *to_parse; GArray *array; + GValue value; gint i; guint32 mac_time; guint64 *dbid; @@ -1175,11 +1176,12 @@ static gboolean playcounts_plist_read (FImport *fimp, GValue *plist_data) array = (GArray*)g_value_get_boxed (to_parse); for (i = 0; i < array->len; i++) { - if (!G_VALUE_HOLDS (g_array_index (array, GValue *, i), G_TYPE_HASH_TABLE)) { + value = g_array_index (array, GValue, i); + if (!G_VALUE_HOLDS (&value, G_TYPE_HASH_TABLE)) { continue; } - track_dict = g_value_get_boxed (g_array_index (array, GValue *, i)); + track_dict = g_value_get_boxed (&value); if (track_dict == NULL) continue; diff --git a/src/itdb_itunesdb.c.playcounts b/src/itdb_itunesdb.c.playcounts new file mode 100644 index 0000000..a177e26 --- /dev/null +++ b/src/itdb_itunesdb.c.playcounts @@ -0,0 +1,8534 @@ +/* +| Copyright (C) 2002-2007 Jorg Schuler +| 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 : +| +| 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); + + 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 +#endif + +#include "db-artwork-parser.h" +#include "itdb_device.h" +#include "itdb_private.h" +#include "itdb_zlib.h" +#include "itdb_plist.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_UNISTD_H +#include +#endif + +#define ITUNESDB_DEBUG 0 +#define ITUNESDB_MHIT_DEBUG 0 + +#define ITUNESDB_COPYBLK (1024*1024*4) /* blocksize for cp () */ + + +/* NOTE for developers: + + Sometimes new MHOD string fields are added by Apple in the + iTunesDB. In that case you need to modify the code in the following + places: + + itdb_itunesdb.c: + - enum MHOD_ID + - get_mhod(): inside the switch() statement. Currently no compiler + warning is given. + - get_mhod_string(): inside the switch() statement. A compiler warning + will help you find it. + - get_playlist(): inside the switch() statement. A compiler warning + will help you find it. + - get_mhit(): inside the switch() statement. A compiler warning + will help you find it. + - mk_mhod(): inside the switch() statement. A compiler warning + will help you find it. + - write_mhsd_tracks(): inside the for() loop. + + itdb_track.c: + - itdb_track_free() + - itdb_track_duplicate() + + itdb_playlists.c: + Analogous to itdb_track.c in case the string is part of the playlist + description. +*/ + +/* Note: some of the comments for the MHOD_IDs are copied verbatim + * from http://ipodlinux.org/ITunesDB */ +enum MHOD_ID { + MHOD_ID_TITLE = 1, + MHOD_ID_PATH = 2, /* file path on iPod (special format) */ + MHOD_ID_ALBUM = 3, + MHOD_ID_ARTIST = 4, + MHOD_ID_GENRE = 5, + MHOD_ID_FILETYPE = 6, +/* MHOD_ID_EQSETTING = 7, */ + MHOD_ID_COMMENT = 8, + /* Category - This is the category ("Technology", "Music", etc.) where + the podcast was located. Introduced in db version 0x0d. */ + MHOD_ID_CATEGORY = 9, + MHOD_ID_COMPOSER = 12, + MHOD_ID_GROUPING = 13, + /* Description text (such as podcast show notes). Accessible by + wselecting the center button on the iPod, where this string is + displayed along with the song title, date, and + timestamp. Introduced in db version 0x0d. */ + MHOD_ID_DESCRIPTION = 14, + /* Podcast Enclosure URL. Note: this is either a UTF-8 or ASCII + encoded string (NOT UTF-16). Also, there is no mhod::length value + for this type. Introduced in db version 0x0d. */ + MHOD_ID_PODCASTURL = 15, + /* Podcast RSS URL. Note: this is either a UTF-8 or ASCII encoded + string (NOT UTF-16). Also, there is no mhod::length value for this + type. Introduced in db version 0x0d. */ + MHOD_ID_PODCASTRSS = 16, + /* Chapter data. This is a m4a-style entry that is used to display + subsongs within a mhit. Introduced in db version 0x0d. */ + MHOD_ID_CHAPTERDATA = 17, + /* Subtitle (usually the same as Description). Introduced in db + version 0x0d. */ + MHOD_ID_SUBTITLE = 18, + /* Show (for TV Shows only). Introduced in db version 0x0d? */ + MHOD_ID_TVSHOW = 19, + /* Episode # (for TV Shows only). Introduced in db version 0x0d? */ + MHOD_ID_TVEPISODE = 20, + /* TV Network (for TV Shows only). Introduced in db version 0x0d? */ + MHOD_ID_TVNETWORK = 21, + /* Album Artist. Introduced in db version 0x13? */ + MHOD_ID_ALBUMARTIST = 22, + /* Sort key for artist. */ + MHOD_ID_SORT_ARTIST = 23, + /* Appears to be a list of keywords pertaining to a track. Introduced + in db version 0x13? */ + MHOD_ID_KEYWORDS = 24, + /* more sort keys, taking precedence over the standard entries if + present */ + MHOD_ID_SORT_TITLE = 27, + MHOD_ID_SORT_ALBUM = 28, + MHOD_ID_SORT_ALBUMARTIST = 29, + MHOD_ID_SORT_COMPOSER = 30, + MHOD_ID_SORT_TVSHOW = 31, + MHOD_ID_SPLPREF = 50, /* settings for smart playlist */ + MHOD_ID_SPLRULES = 51, /* rules for smart playlist */ + MHOD_ID_LIBPLAYLISTINDEX = 52, /* Library Playlist Index */ + MHOD_ID_LIBPLAYLISTJUMPTABLE=53, + MHOD_ID_PLAYLIST = 100, + MHOD_ID_ALBUM_ALBUM = 200, /* MHODs for the MHIAs */ + MHOD_ID_ALBUM_ARTIST = 201, + MHOD_ID_ALBUM_SORT_ARTIST = 202, + MHOD_ID_ALBUM_ARTIST_MHII = 300, +}; + + +/* Used as first iPod ID when renumbering (re-assigning) the iPod + (track) IDs before writing out the iTunesDB. */ +static const gint FIRST_IPOD_ID=52; + + +enum MHOD52_SORTTYPE { + MHOD52_SORTTYPE_TITLE = 0x03, + MHOD52_SORTTYPE_ALBUM = 0x04, + MHOD52_SORTTYPE_ARTIST = 0x05, + MHOD52_SORTTYPE_GENRE = 0x07, + MHOD52_SORTTYPE_COMPOSER = 0x12/*, + MHOD52_SORTTYPE_TVSHOW = 0x1d, + MHOD52_SORTTYPE_TVSEASON = 0x1e, + MHOD52_SORTTYPE_TVEPISODE= 0x1f*/ +}; + +struct mhod52track +{ + gchar *album; + gchar *title; + gchar *artist; + gchar *genre; + gchar *composer; + gint track_nr; + gint cd_nr; + gint index; + gint numtracks; + gunichar2 letter_album; + gunichar2 letter_title; + gunichar2 letter_artist; + gunichar2 letter_genre; + gunichar2 letter_composer; +}; + +struct mhod53_entry +{ + gunichar2 letter; + gint32 start; + gint32 count; +}; + +struct _MHODData +{ + gboolean valid; + gint32 type; + union + { + gint32 track_pos; + gchar *string; + Itdb_Chapterdata *chapterdata; + Itdb_SPLPref *splpref; + Itdb_SPLRules *splrules; + GList *mhod52coltracks; + } data; + enum MHOD52_SORTTYPE mhod52sorttype; + GList *mhod53_list; +}; + +typedef struct _MHODData MHODData; + +struct _PosEntry { + guint32 trackid; + gint32 track_pos; +}; + +typedef struct _PosEntry PosEntry; + +/* Declarations */ +static gboolean itdb_create_directories (Itdb_Device *device, GError **error); + +/* 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; +} + + +static guint16 raw_get16lint (FContents *cts, glong seek); +static guint32 raw_get24lint (FContents *cts, glong seek); +static guint32 raw_get32lint (FContents *cts, glong seek); +static guint64 raw_get64lint (FContents *cts, glong seek); +static float raw_get32lfloat (FContents *cts, glong seek); +static guint16 raw_get16bint (FContents *cts, glong seek); +static guint32 raw_get24bint (FContents *cts, glong seek); +static guint32 raw_get32bint (FContents *cts, glong seek); +static guint64 raw_get64bint (FContents *cts, glong seek); +static float raw_get32bfloat (FContents *cts, glong seek); + +static gboolean is_shuffle_2g (Itdb_Device *device); + +static const ByteReader LITTLE_ENDIAN_READER = +{ + raw_get16lint, raw_get24lint, raw_get32lint, raw_get64lint, raw_get32lfloat +}; +static const ByteReader BIG_ENDIAN_READER = +{ + raw_get16bint, raw_get24bint, raw_get32bint, raw_get64bint, raw_get32bfloat +}; + +static void fcontents_set_reversed(FContents *cts, gboolean reversed) +{ + cts->reversed = reversed; + if (!reversed) + { + memcpy(&cts->le_reader, &LITTLE_ENDIAN_READER, sizeof (ByteReader)); + memcpy(&cts->be_reader, &BIG_ENDIAN_READER, sizeof (ByteReader)); + } + else + { + memcpy(&cts->le_reader, &BIG_ENDIAN_READER, sizeof (ByteReader)); + memcpy(&cts->be_reader, &LITTLE_ENDIAN_READER, sizeof (ByteReader)); + } +} + +/* 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); + fcontents_set_reversed (cts, FALSE); + + 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); + } +} + +static void itdb_fsync (void) +{ +#ifndef WIN32 + sync(); +#endif +} + +/* 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.*/ + +/** + * itdb_resolve_path: + * @root: in local encoding + * @components: in utf8 + * + * Resolve the path to a track on the iPod + * + * 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. + * + * Returns: path to track on the iPod or NULL. + */ +gchar * itdb_resolve_path (const gchar *root, + const gchar * const * components) +{ + gchar *good_path; + guint32 i; + + if (!root) return NULL; + good_path = g_strdup (root); + + 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; + gchar *file_stdcase; + gboolean found; + gchar *new_good_path; + + file_utf8 = g_filename_to_utf8(dir_file,-1,NULL,NULL,NULL); + if (file_utf8 == NULL) { + continue; + } + file_stdcase = g_utf8_casefold(file_utf8,-1); + g_free(file_utf8); + found = !g_utf8_collate(file_stdcase,component_stdcase); + g_free(file_stdcase); + if(!found) + { + /* This is not the matching entry */ + continue; + } + + new_good_path = g_build_filename(good_path,dir_file,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; + + g_free (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, const gchar *data, + glong seek, glong len) +{ + if (check_seek (cts, seek, len)) + { + gint i; + for (i=0; icontents[seek+i] != data[i]) return FALSE; + } + return TRUE; + } + return FALSE; +} + + +/* Compare 4 bytes of @header with 4 bytes at @seek taking into + * consideration the status of cts->reversed */ +static gboolean check_header_seek (FContents *cts, const gchar *data, + glong seek) +{ + gchar rdata[4]; + gint i, offset, sign; + + g_return_val_if_fail (cts, FALSE); + g_return_val_if_fail (data, FALSE); + /* reverse data for compare if necessary */ + if (cts->reversed) + { + offset = 3; + sign = -1; + } + else + { + offset = 0; + sign = 1; + } + for (i=0; i<4; ++i) + { + rdata[i] = data[offset + sign*i]; + } + + return cmp_n_bytes_seek (cts, rdata, seek, 4); +} + + +/* ------------------------------------------------------------ + Not Endian Dependent + ------------------------------------------------------------ */ + +/* Returns the 1-byte number stored at position @seek. On error the + * GError in @cts is set. */ +static inline guint8 get8int (FContents *cts, glong seek) +{ + guint8 n=0; + + if (check_seek (cts, seek, 1)) + { + n = cts->contents[seek]; + } + return n; +} + + +/* ------------------------------------------------------------ + Little Endian + ------------------------------------------------------------ */ + +/* Get the 2-byte-number stored at position "seek" in little endian + encoding. On error the GError in @cts is set. */ +static guint16 raw_get16lint (FContents *cts, glong seek) +{ + guint16 n=0; + + if (check_seek (cts, seek, 2)) + { + memcpy (&n, &cts->contents[seek], 2); + n = GUINT16_FROM_LE (n); + } + return n; +} + +/* Get the 3-byte-number stored at position "seek" in little endian + encoding. On error the GError in @cts is set. */ +static guint32 raw_get24lint (FContents *cts, glong seek) +{ + guint32 n=0; + + if (check_seek (cts, seek, 3)) + { + n = ((guint32)get8int (cts, seek+0)) + + (((guint32)get8int (cts, seek+1)) >> 8) + + (((guint32)get8int (cts, seek+2)) >> 16); + } + 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 raw_get32lint (FContents *cts, glong seek) +{ + guint32 n=0; + + if (check_seek (cts, seek, 4)) + { + memcpy (&n, &cts->contents[seek], 4); + n = GUINT32_FROM_LE (n); + } + return n; +} + +/* Get 4 byte floating number */ +static float raw_get32lfloat (FContents *cts, glong seek) +{ + union + { + guint32 i; + float f; + } flt; + + g_return_val_if_fail (sizeof (float) == 4, 0); + + flt.i = raw_get32lint (cts, seek); + + return flt.f; +} + + +/* Get the 8-byte-number stored at position "seek" in little endian + encoding. On error the GError in @cts is set. */ +static guint64 raw_get64lint (FContents *cts, glong seek) +{ + guint64 n=0; + + if (check_seek (cts, seek, 8)) + { + memcpy (&n, &cts->contents[seek], 8); + n = GUINT64_FROM_LE (n); + } + return n; +} + + +/* ------------------------------------------------------------ + Big Endian + ------------------------------------------------------------ */ + +/* Get the 2-byte-number stored at position "seek" in little endian + encoding. On error the GError in @cts is set. */ +static guint16 raw_get16bint (FContents *cts, glong seek) +{ + guint16 n=0; + + if (check_seek (cts, seek, 2)) + { + memcpy (&n, &cts->contents[seek], 2); + n = GUINT16_FROM_BE (n); + } + return n; +} + +/* Get the 3-byte-number stored at position "seek" in big endian + encoding. On error the GError in @cts is set. */ +static guint32 raw_get24bint (FContents *cts, glong seek) +{ + guint32 n=0; + + if (check_seek (cts, seek, 3)) + { + n = ((guint32)get8int (cts, seek+2)) + + (((guint32)get8int (cts, seek+1)) >> 8) + + (((guint32)get8int (cts, seek+0)) >> 16); + } + 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 raw_get32bint (FContents *cts, glong seek) +{ + guint32 n=0; + + if (check_seek (cts, seek, 4)) + { + memcpy (&n, &cts->contents[seek], 4); + n = GUINT32_FROM_BE (n); + } + return n; +} + +/* Get 4 byte floating number */ +static float raw_get32bfloat (FContents *cts, glong seek) +{ + union + { + guint32 i; + float f; + } flt; + + g_return_val_if_fail (sizeof (float) == 4, 0); + + flt.i = raw_get32bint (cts, seek); + + return flt.f; +} + +/* Get the 8-byte-number stored at position "seek" in big endian + encoding. On error the GError in @cts is set. */ +static guint64 raw_get64bint (FContents *cts, glong seek) +{ + guint64 n=0; + + if (check_seek (cts, seek, 8)) + { + memcpy (&n, &cts->contents[seek], 8); + n = GUINT64_FROM_BE (n); + } + return n; +} + + +/* ------------------------------------------------------------ + Little Endian + ------------------------------------------------------------ */ + +static inline guint16 get16lint (FContents *cts, glong seek) +{ + return cts->le_reader.get16int (cts, seek); +} + +static inline guint32 get24lint (FContents *cts, glong seek) +{ + return cts->le_reader.get24int (cts, seek); +} +#if 0 +static inline guint32 get24bint (FContents *cts, glong seek) +{ + return cts->be_reader.get24int (cts, seek); +} +#endif +static inline guint32 get32lint (FContents *cts, glong seek) +{ + return cts->le_reader.get32int (cts, seek); +} + +static inline float get32lfloat (FContents *cts, glong seek) +{ + return cts->le_reader.get32float (cts, seek); +} + +static inline guint64 get64lint (FContents *cts, glong seek) +{ + return cts->le_reader.get64int (cts, seek); +} + + + +/* ------------------------------------------------------------ + Big Endian + ------------------------------------------------------------ */ + +static inline guint16 get16bint (FContents *cts, glong seek) +{ + return cts->be_reader.get16int (cts, seek); +} + +static inline guint32 get32bint (FContents *cts, glong seek) +{ + return cts->be_reader.get32int (cts, seek); +} + +#if 0 +static inline float get32bfloat (FContents *cts, glong seek) +{ + return cts->be_reader.get32float (cts, seek); +} +#endif + +static inline guint64 get64bint (FContents *cts, glong seek) +{ + return cts->be_reader.get64int (cts, seek); +} + +/* Try to convert from UTF-16 to UTF-8 and handle partial characters + * at the end of the string */ +static char *utf16_to_utf8_with_partial (gunichar2 *entry_utf16) +{ + GError *error = NULL; + char *entry_utf8; + glong items_read; + + entry_utf8 = g_utf16_to_utf8 (entry_utf16, -1, &items_read, NULL, &error); + if (entry_utf8 == NULL) { + if (g_error_matches (error, G_CONVERT_ERROR, G_CONVERT_ERROR_PARTIAL_INPUT)) { + entry_utf8 = g_utf16_to_utf8 (entry_utf16, items_read, NULL, NULL, NULL); + } + g_error_free (error); + } + + return entry_utf8; +} + +/* 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; utf16_string[i]; 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 and chapter data 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; utf16_string[i]; 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_take_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_take_next (fimp))) g_free (playcount); +} + + +/* called by playcounts_init */ +static gboolean playcounts_read (FImport *fimp, FContents *cts) +{ + GList* playcounts = NULL; + guint32 header_length, entry_length, entry_num, i=0; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (cts, FALSE); + + if (!check_header_seek (cts, "mhdp", 0)) + { + if (cts->error) + { + g_propagate_error (&fimp->error, cts->error); + return FALSE; + } + fcontents_set_reversed (cts, TRUE); + if (!check_header_seek (cts, "mhdp", 0)) + { + if (cts->error) + { + g_propagate_error (&fimp->error, cts->error); + return FALSE; + } + 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), 0x14 (iTunesDB version 0x0d) or 0x1c (iTunesDB + * version 0x13) 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; iplaycount = get32lint (cts, seek); + mac_time = get32lint (cts, seek+4); + playcount->time_played = device_time_mac_to_time_t (fimp->itdb->device, mac_time); + playcount->bookmark_time = get32lint (cts, seek+8); + + /* rating only exists if the entry length is at least 0x10 */ + if (entry_length >= 0x10) + { + playcount->rating = get32lint (cts, seek+12); + } + else + { + playcount->rating = NO_PLAYCOUNT; + } + /* unk16 only exists if the entry length is at least 0x14 */ + if (entry_length >= 0x14) + { + playcount->pc_unk16 = get32lint (cts, seek+16); + } + /* skip_count and last_skipped only exists if the entry length + is at least 0x1c */ + if (entry_length >= 0x1c) + { + playcount->skipcount = get32lint (cts, seek+20); + mac_time = get32lint (cts, seek+24); + playcount->last_skipped = device_time_mac_to_time_t (fimp->itdb->device, + mac_time); + + } + } + fimp->playcounts = g_list_reverse(playcounts); + return TRUE; +} + + +/* called by playcounts_init */ +static gboolean itunesstats_read (FImport *fimp, FContents *cts) +{ + GList* playcounts = NULL; + guint32 entry_num, entry_length, i=0; + struct playcount *playcount; + glong seek = 0; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (cts, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + g_return_val_if_fail (fimp->itdb->device, FALSE); + + if ( is_shuffle_2g (fimp->itdb->device)) + { + /* Old iTunesStats format */ + + /* number of entries */ + entry_num = get24lint (cts, seek); + CHECK_ERROR (fimp, FALSE); + seek = 6; + for (i=0; ierror, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesStats file ('%s'): entry length smaller than expected (%d<18)."), + cts->filename, entry_length); + return FALSE; + } + playcount = g_new0 (struct playcount, 1); + playcounts = g_list_prepend (playcounts, playcount); + /* 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 */ + playcount->bookmark_time = get24lint (cts, seek+3); + CHECK_ERROR (fimp, FALSE); + playcount->st_unk06 = get24lint (cts, seek+6); + CHECK_ERROR (fimp, FALSE); + playcount->st_unk09 = get24lint (cts, seek+9); + CHECK_ERROR (fimp, FALSE); + playcount->playcount = get24lint (cts, seek+12); + CHECK_ERROR (fimp, FALSE); + playcount->skipped = get24lint (cts, seek+15); + CHECK_ERROR (fimp, FALSE); + + playcount->rating = NO_PLAYCOUNT; + + seek += entry_length; + } + } else { + /* New iTunesStats format */ + entry_num = get32lint (cts, seek); /* Number of entries */ + CHECK_ERROR (fimp, FALSE); + + seek = 8; + for (i=0; ierror, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesStats file ('%s'): entry length smaller than expected (%d<32)."), + cts->filename, entry_length); + return FALSE; + } + playcount = g_new0 (struct playcount, 1); + playcounts = g_list_prepend (playcounts, playcount); + + playcount->bookmark_time = get32lint (cts, seek+4); + CHECK_ERROR (fimp, FALSE); + playcount->playcount = get32lint (cts, seek+8); + CHECK_ERROR (fimp, FALSE); + playcount->time_played = get32lint (cts, seek+12); + CHECK_ERROR (fimp, FALSE); + playcount->skipped = get32lint (cts, seek+16); + CHECK_ERROR (fimp, FALSE); + playcount->last_skipped = get32lint (cts, seek+20); + CHECK_ERROR (fimp, FALSE); + /* 8 unknown bytes follow but it is unlikely they are + * the same as the unknown fields in the old format. + * I have only seen zeros in those spots so far so I'm + * ignoring them for now. */ + } + } + fimp->playcounts = g_list_reverse(playcounts); + return TRUE; +} + +static gint64 playcounts_plist_get_gint64 (GHashTable *track_dict, + const char *key) +{ + GValue *value; + + value = g_hash_table_lookup (track_dict, key); + if (value && G_VALUE_HOLDS (value, G_TYPE_INT64)) { + return g_value_get_int64 (value); + } + + return 0; +} + +#ifndef HAVE_G_INT64_EQUAL +static gboolean g_int64_equal (gconstpointer v1, gconstpointer v2) +{ + return *((const gint64*) v1) == *((const gint64*) v2); +} +#endif + +#ifndef HAVE_G_INT64_HASH +static guint g_int64_hash (gconstpointer v) +{ + return (guint) *(const gint64*) v; +} +#endif + +/* called by playcounts_init */ +static gboolean playcounts_plist_read (FImport *fimp, GValue *plist_data) +{ + GHashTable *playcounts; + struct playcount *playcount; + GHashTable *pc_dict, *track_dict; + GValue *to_parse; + GArray *array; + gint i; + guint32 mac_time; + guint64 *dbid; + + g_return_val_if_fail (G_VALUE_HOLDS (plist_data, G_TYPE_HASH_TABLE), FALSE); + pc_dict = g_value_get_boxed (plist_data); + + to_parse = g_hash_table_lookup (pc_dict, "tracks"); + if (to_parse == NULL) { + return FALSE; + } + if (!G_VALUE_HOLDS (to_parse, G_TYPE_ARRAY)) { + return FALSE; + } + + playcounts = g_hash_table_new_full (g_int64_hash, g_int64_equal, g_free, g_free); + + array = (GArray*)g_value_get_boxed (to_parse); + for (i = 0; i < array->len; i++) { + if (!G_VALUE_HOLDS (g_array_index (array, GValue *, i), G_TYPE_HASH_TABLE)) { + continue; + } + + track_dict = g_value_get_boxed (g_array_index (array, GValue *, i)); + if (track_dict == NULL) + continue; + + to_parse = g_hash_table_lookup (track_dict, "persistentID"); + if (!to_parse) + continue; + + dbid = g_new0 (guint64, 1); + if (!G_VALUE_HOLDS (to_parse, G_TYPE_INT64)) + continue; + *dbid = g_value_get_int64 (to_parse); + playcount = g_new0 (struct playcount, 1); + g_hash_table_insert (playcounts, dbid, playcount); + + playcount->bookmark_time = playcounts_plist_get_gint64 (track_dict, "bookmarkTimeInMS"); + playcount->playcount = playcounts_plist_get_gint64 (track_dict, "playCount"); + mac_time = playcounts_plist_get_gint64 (track_dict, "playMacOSDate"); + playcount->time_played = device_time_mac_to_time_t (fimp->itdb->device, mac_time); + playcount->skipcount = playcounts_plist_get_gint64 (track_dict, "skipCount"); + mac_time = playcounts_plist_get_gint64 (track_dict, "skipMacOSDate"); + playcount->last_skipped = device_time_mac_to_time_t (fimp->itdb->device, mac_time); + playcount->rating = playcounts_plist_get_gint64 (track_dict, "userRating"); + if (!playcount->rating) + playcount->rating = NO_PLAYCOUNT; + + to_parse = g_hash_table_lookup (track_dict, "playedState"); + if (to_parse && G_VALUE_HOLDS (to_parse, G_TYPE_BOOLEAN)) { + ; /* What do we do with this? */ + } + } + fimp->pcounts2 = playcounts; + return TRUE; +} + +/* Delete the file containing the playcounts, once we saved the database, + * the recent playcounts will have been added to the database, so this + * file is no longer useful. */ +static void playcounts_reset (const char *mountpoint) +{ + const gchar *dirs[] = { "Play Counts", "iTunesStats", + "PlayCounts.plist", NULL }; + const gchar **it; + + for (it = dirs; *it != NULL; it++) { + char *playcounts_path; + const gchar *components[] = { *it, NULL }; + playcounts_path = itdb_resolve_path (mountpoint, components); + if (playcounts_path != NULL) { + g_unlink (playcounts_path); + g_free (playcounts_path); + } + } +} + +/* 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. If no Play Count file is present, attempt to + * read the iTunesStats file instead, which is used on the Shuffle for + * the same purpose. + * + * 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 *plc[] = {"Play Counts", NULL}; + const gchar *ist[] = {"iTunesStats", NULL}; + const gchar *plcpl[] = {"PlayCounts.plist", NULL}; + gchar *plcname, *dirname, *istname, *plcplname; + gboolean result=TRUE; + struct stat filestat; + FContents *cts; + GValue *plist_data; + + 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, plc); + istname = itdb_resolve_path (dirname, ist); + plcplname = itdb_resolve_path (dirname, plcpl); + + g_free (dirname); + + /* skip if no playcounts file is present */ + if (plcname) + { + /* skip if playcounts file has zero-length (often happens after + * dosfsck) */ + stat (plcname, &filestat); + if (filestat.st_size >= 0x60) + { + cts = fcontents_read (plcname, &fimp->error); + if (cts) + { + result = playcounts_read (fimp, cts); + fcontents_free (cts); + } + else + { + result = FALSE; + } + } + } + else if (istname) + { + /* skip if iTunesStats file has zero-length (often happens after + * dosfsck) */ + stat (istname, &filestat); + if (filestat.st_size >= 0x06) + { + cts = fcontents_read (istname, &fimp->error); + if (cts) + { + result = itunesstats_read (fimp, cts); + fcontents_free (cts); + } + else + { + result = FALSE; + } + } + } + else if (plcplname) + { + /* skip if PlayCounts.plist file has zero-length */ + stat (plcplname, &filestat); + if (filestat.st_size > 0) + { + plist_data = itdb_plist_parse_from_file (plcplname, &fimp->error); + if (plist_data) + { + result = playcounts_plist_read (fimp, plist_data); + g_value_unset (plist_data); + } + else + { + result = FALSE; + } + } + } + + g_free (plcname); + g_free (istname); + g_free (plcplname); + + 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->fcontents) fcontents_free (fimp->fcontents); + g_list_free (fimp->pos_glist); + g_list_free (fimp->tracks); + playcounts_free (fimp); + if (fimp->pcounts2 != NULL) { + g_hash_table_destroy (fimp->pcounts2); + } + g_free (fimp); + } +} + +/** + * itdb_free: + * @itdb: an #Itdb_iTunesDB + * + * Free the memory taken by @itdb. + */ +void itdb_free (Itdb_iTunesDB *itdb) +{ + if (itdb) + { + g_list_foreach (itdb->playlists, + (GFunc)(itdb_playlist_free), NULL); + + if (itdb->priv) { + if (itdb->priv->mhsd5_playlists) + { + g_list_foreach (itdb->priv->mhsd5_playlists, + (GFunc)(itdb_playlist_free), NULL); + } + + g_free(itdb->priv->genius_cuid); + } + + g_list_free (itdb->playlists); + g_list_foreach (itdb->tracks, + (GFunc)(itdb_track_free), NULL); + g_list_free (itdb->tracks); + g_free (itdb->filename); + itdb_device_free (itdb->device); + if (itdb->userdata && itdb->userdata_destroy) + (*itdb->userdata_destroy) (itdb->userdata); + g_free (itdb->priv); + g_free (itdb); + } +} + +/** + * itdb_duplicate: + * @itdb: an #Itdb_iTunesDB + * + * Duplicate @itdb + * FIXME: not implemented yet + * + * Returns: always return NULL since it's unimplemented + */ +Itdb_iTunesDB *itdb_duplicate (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb, NULL); + /* FIXME: not yet implemented */ + g_return_val_if_reached (NULL); +} + +/** + * itdb_playlists_number: + * @itdb: an #Itdb_iTunesDB + * + * Counts the number of playlists stored in @itdb + * + * Returns: the number of playlists in @itdb (including the master + * playlist) + */ +guint32 itdb_playlists_number (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb, 0); + + return g_list_length (itdb->playlists); +} + +/** + * itdb_tracks_number: + * @itdb: an #Itdb_iTunesDB + * + * Counts the number of tracks stored in @itdb + * + * Returns: the number of tracks in @itdb + */ +guint32 itdb_tracks_number (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb, 0); + + return g_list_length (itdb->tracks); +} + +/** + * itdb_tracks_number_nontransferred: + * @itdb: an #Itdb_iTunesDB + * + * Counts the number of non-transferred tracks in @itdb + * + * Returns: the number of tracks in @itdb that haven't been transferred + * to the iPod yet (ie the number of #Itdb_Track in which the transferred field + * is false) + */ +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; +} + +/** + * itdb_new: + * + * Creates a new Itdb_iTunesDB with the unknowns filled in to reasonable + * values. + * + * Returns: a newly created Itdb_iTunesDB to be freed with itdb_free() + * when it's no longer needed + */ +Itdb_iTunesDB *itdb_new (void) +{ + Itdb_iTunesDB *itdb; + +#if !GLIB_CHECK_VERSION(2, 36, 0) + /* g_type_init() is obsolete with newer glib versions */ + static GOnce g_type_init_once = G_ONCE_INIT; + g_once (&g_type_init_once, (GThreadFunc)g_type_init, NULL); +#endif + + itdb = g_new0 (Itdb_iTunesDB, 1); + itdb->priv = g_new0 (Itdb_iTunesDB_Private, 1); + itdb->device = itdb_device_new (); + itdb->version = 0x13; + itdb->id = ((guint64)g_random_int () << 32) | + ((guint64)g_random_int ()); + itdb->priv->pid = ((guint64)g_random_int () << 32) | + ((guint64)g_random_int ()); + itdb->priv->lang = 0x656e; + itdb->priv->platform = 1; /* Mac */ + 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, guint32 *ml) +{ + gint32 type = -1; + +#if ITUNESDB_DEBUG + fprintf(stderr, "get_mhod_type seek: %x\n", (int)seek); +#endif + + if (ml) *ml = -1; + + if (check_header_seek (cts, "mhod", seek)) + { + 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; +} + +static char *extract_mhod_string (FContents *cts, glong seek) +{ + gunichar2 *entry_utf16; + char *entry_utf8; + gint string_type; + gsize len; + + /* type of string: 0x02: UTF8, 0x01 or 0x00: UTF16 LE */ + string_type = get32lint (cts, seek); + len = get32lint (cts, seek+4); /* length of string */ + g_return_val_if_fail (len < G_MAXUINT - 2, NULL); + if (string_type != 0x02) { + /* UTF-16 string */ + entry_utf16 = g_new0 (gunichar2, (len+2)/2); + if (seek_get_n_bytes (cts, (gchar *)entry_utf16, seek+16, len)) { + fixup_little_utf16 (entry_utf16); + entry_utf8 = utf16_to_utf8_with_partial (entry_utf16); + g_free (entry_utf16); + } else { + g_free (entry_utf16); + return NULL; + } + } else { + /* UTF-8 string */ + entry_utf8 = g_new0 (gchar, len+1); + if (!seek_get_n_bytes (cts, entry_utf8, seek+16, len)) { + g_free (entry_utf8); + return NULL; + } + } + + if ((entry_utf8 != NULL) && g_utf8_validate (entry_utf8, -1, NULL)) { + return entry_utf8; + } else { + g_free (entry_utf8); + return NULL; + } +} + +/* Returns the contents of the mhod at position @mhod_seek. This can + be a simple string or something more complicated as in the case for + Itdb_SPLPREF OR Itdb_SPLRULES. + + *mhod_len is set to the total length of the mhod (-1 in case an + *error occured). + + MHODData.valid is set to FALSE in case of any error. cts->error + will be set accordingly. + + MHODData.type is set to the type of the mhod. The data (or a + pointer to the data) will be stored in + .playlist_id/.string/.chapterdata/.splp/.splrs +*/ + +static MHODData get_mhod (FImport *fimp, glong mhod_seek, guint32 *ml) +{ + MHODData result; + gint32 xl; + guint32 mhod_len; + gint32 header_length; + gulong seek; + FContents *cts; + + cts = fimp->fcontents; + + result.valid = FALSE; + result.type = -1; + g_return_val_if_fail (ml, result); + *ml = -1; + + g_return_val_if_fail (cts, result); + g_return_val_if_fail (!cts->error, result); + +#if ITUNESDB_DEBUG + fprintf(stderr, "get_mhod seek: %ld\n", mhod_seek); +#endif + + result.type = get_mhod_type (cts, mhod_seek, &mhod_len); + + if (mhod_len == -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 result; + } + + if (!check_seek (cts, mhod_seek, mhod_len)) + return result; + + + header_length = get32lint (cts, mhod_seek+4); /* header length */ + + seek = mhod_seek + header_length; + +#if ITUNESDB_DEBUG + fprintf(stderr, "ml: %x type: %x\n", *ml, result.type); +#endif + + switch ((enum MHOD_ID)result.type) + { + case MHOD_ID_LIBPLAYLISTINDEX: + /* this is not yet supported */ + case MHOD_ID_PLAYLIST: + /* return the position indicator */ + result.data.track_pos = get32lint (cts, mhod_seek+24); + break; + case MHOD_ID_TITLE: + case MHOD_ID_PATH: + case MHOD_ID_ALBUM: + case MHOD_ID_ARTIST: + case MHOD_ID_GENRE: + case MHOD_ID_FILETYPE: + case MHOD_ID_COMMENT: + case MHOD_ID_CATEGORY: + case MHOD_ID_COMPOSER: + case MHOD_ID_GROUPING: + case MHOD_ID_DESCRIPTION: + case MHOD_ID_SUBTITLE: + case MHOD_ID_TVSHOW: + case MHOD_ID_TVEPISODE: + case MHOD_ID_TVNETWORK: + case MHOD_ID_ALBUMARTIST: + case MHOD_ID_KEYWORDS: + case MHOD_ID_SORT_ARTIST: + case MHOD_ID_SORT_TITLE: + case MHOD_ID_SORT_ALBUM: + case MHOD_ID_SORT_ALBUMARTIST: + case MHOD_ID_SORT_COMPOSER: + case MHOD_ID_SORT_TVSHOW: + result.data.string = extract_mhod_string (cts, seek); + if (result.data.string == NULL) { + *ml = mhod_len; + return result; + } + break; + case MHOD_ID_PODCASTURL: + case MHOD_ID_PODCASTRSS: + /* length of string */ + xl = mhod_len - header_length; + g_return_val_if_fail (xl < G_MAXUINT - 1, result); + result.data.string = g_new0 (gchar, xl+1); + if (!seek_get_n_bytes (cts, result.data.string, seek, xl)) + { + g_free (result.data.string); + return result; /* *ml==-1, result.valid==FALSE */ + } + break; + case MHOD_ID_CHAPTERDATA: + result.data.chapterdata = itdb_chapterdata_new(); + result.data.chapterdata->unk024 = get32lint (cts, seek); + result.data.chapterdata->unk028 = get32lint (cts, seek+4); + result.data.chapterdata->unk032 = get32lint (cts, seek+8); + seek += 12; /* get past unks */ + if (check_header_seek (cts, "sean", seek+4)) + { + gint i; + guint32 numchapters; + numchapters = get32bint (cts, seek+12) - 1; /* minus 1 for hedr atom */ + seek += 20; /* move to atom data */ + for (i=0; iliveupdate = get8int (cts, seek); + result.data.splpref->checkrules = get8int (cts, seek+1); + result.data.splpref->checklimits = get8int (cts, seek+2); + result.data.splpref->limittype = get8int (cts, seek+3); + result.data.splpref->limitsort = get8int (cts, seek+4); + result.data.splpref->limitvalue = get32lint (cts, seek+8); + result.data.splpref->matchcheckedonly = get8int (cts, seek+12); + /* if the opposite flag is on (seek+13), set limitsort's high + bit -- see note in itunesdb.h for more info */ + if (get8int (cts, seek+13)) + result.data.splpref->limitsort |= 0x80000000; + break; + case MHOD_ID_SPLRULES: /* Rules for smart playlist */ + if (check_header_seek (cts, "SLst", seek)) + { + /* !!! for some reason the SLst part is the only part of the + iTunesDB with big-endian encoding, including UTF16 + strings */ + gint i; + guint32 numrules; + if (!check_seek (cts, seek, 136)) + return result; /* *ml==-1, result.valid==FALSE */ + result.data.splrules = g_new0 (Itdb_SPLRules, 1); + result.data.splrules->unk004 = get32bint (cts, seek+4); + numrules = get32bint (cts, seek+8); + result.data.splrules->match_operator = get32bint (cts, seek+12); + seek += 136; /* I can't find this value stored in the + iTunesDB :-( */ + for (i=0; irules = g_list_append ( + result.data.splrules->rules, splr); + if (!check_seek (cts, seek, 56)) + goto splrules_error; + splr->field = get32bint (cts, seek); + splr->action = get32bint (cts, seek+4); + + if (!itdb_spl_action_known (splr->action)) + { + g_warning (_("Unknown smart rule action at %ld: %x. Trying to continue.\n"), seek, splr->action); + } + + seek += 52; + length = get32bint (cts, seek); + g_return_val_if_fail (length < G_MAXUINT-2, result); + + ft = itdb_splr_get_field_type (splr); + switch (ft) + { + case ITDB_SPLFT_STRING: + string_utf16 = g_new0 (gunichar2, (length+2)/2); + if (!seek_get_n_bytes (cts, (gchar *)string_utf16, + seek+4, length)) + { + g_free (string_utf16); + goto splrules_error; + } + fixup_big_utf16 (string_utf16); + splr->string = g_utf16_to_utf8 ( + string_utf16, -1, NULL, NULL, NULL); + g_free (string_utf16); + break; + case ITDB_SPLFT_INT: + case ITDB_SPLFT_DATE: + case ITDB_SPLFT_BOOLEAN: + case ITDB_SPLFT_PLAYLIST: + case ITDB_SPLFT_UNKNOWN: + case ITDB_SPLFT_BINARY_AND: + if (length != 0x44) + { + g_warning (_("Length of smart playlist rule field (%d) not as expected. Trying to continue anyhow.\n"), length); + } + if (!check_seek (cts, seek, 72)) + goto splrules_error; + splr->fromvalue = get64bint (cts, seek+4); + splr->fromdate = get64bint (cts, seek+12); + splr->fromunits = get64bint (cts, seek+20); + splr->tovalue = get64bint (cts, seek+28); + splr->todate = get64bint (cts, seek+36); + splr->tounits = get64bint (cts, seek+44); + /* ITDB_SPLFIELD_PLAYLIST seems to use these unknowns*/ + splr->unk052 = get32bint (cts, seek+52); + splr->unk056 = get32bint (cts, seek+56); + splr->unk060 = get32bint (cts, seek+60); + splr->unk064 = get32bint (cts, seek+64); + splr->unk068 = get32bint (cts, seek+68); + + if (ft == ITDB_SPLFT_DATE) { + ItdbSPLActionType at; + at = itdb_splr_get_action_type (splr); + if ((at == ITDB_SPLAT_RANGE_DATE) || + (at == ITDB_SPLAT_DATE)) + { + Itdb_iTunesDB *itdb = fimp->itdb; + splr->fromvalue = device_time_mac_to_time_t (itdb->device, + splr->fromvalue); + splr->tovalue = device_time_mac_to_time_t (itdb->device, + splr->tovalue); + } + } + + break; + } + seek += length+4; + } + } + else + { + if (!cts->error) + { /* set error */ + g_set_error (&cts->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: no SLst at offset %ld in file '%s'."), + seek, cts->filename); + } + return result; /* *ml==-1, result.valid==FALSE */ + } + break; + splrules_error: + g_list_foreach (result.data.splrules->rules, + (GFunc)(itdb_splr_free), NULL); + g_list_free (result.data.splrules->rules); + g_free (result.data.splrules); + return result; /* *ml==-1, result.valid==FALSE */ + default: + g_warning (_("Encountered unknown MHOD type (%d) while parsing the iTunesDB. Ignoring.\n\n"), result.type); + *ml = mhod_len; + return result; + } + + *ml = mhod_len; + result.valid = TRUE; + 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 + UTF8). 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 gchar *get_mhod_string (FImport *fimp, glong seek, guint32 *ml, gint32 *mty) +{ + MHODData mhoddata; + FContents *cts; + + cts = fimp->fcontents; + + *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_FILETYPE: + case MHOD_ID_COMMENT: + case MHOD_ID_CATEGORY: + case MHOD_ID_COMPOSER: + case MHOD_ID_GROUPING: + case MHOD_ID_DESCRIPTION: + case MHOD_ID_PODCASTURL: + case MHOD_ID_PODCASTRSS: + case MHOD_ID_SUBTITLE: + case MHOD_ID_TVSHOW: + case MHOD_ID_TVEPISODE: + case MHOD_ID_TVNETWORK: + case MHOD_ID_ALBUMARTIST: + case MHOD_ID_KEYWORDS: + case MHOD_ID_SORT_ARTIST: + case MHOD_ID_SORT_TITLE: + case MHOD_ID_SORT_ALBUM: + case MHOD_ID_SORT_ALBUMARTIST: + case MHOD_ID_SORT_COMPOSER: + case MHOD_ID_SORT_TVSHOW: + case MHOD_ID_ALBUM_ALBUM: + case MHOD_ID_ALBUM_ARTIST: + case MHOD_ID_ALBUM_ARTIST_MHII: + case MHOD_ID_ALBUM_SORT_ARTIST: + mhoddata = get_mhod (fimp, seek, ml); + if ((*ml != -1) && mhoddata.valid) + return mhoddata.data.string; + else + return NULL; + case MHOD_ID_SPLPREF: + case MHOD_ID_SPLRULES: + case MHOD_ID_LIBPLAYLISTINDEX: + case MHOD_ID_PLAYLIST: + case MHOD_ID_CHAPTERDATA: + case MHOD_ID_LIBPLAYLISTJUMPTABLE: + /* these do not have a string entry */ + return NULL; + } +#if ITUNESDB_MHIT_DEBUG + fprintf (stderr, "Ignoring unknown MHOD of type %d at offset %ld\n", *mty, seek); +#endif + return NULL; +} + + +/* convenience function: set error for zero length hunk */ +static void set_error_zero_length_hunk (GError **error, glong seek, + const gchar *filename) +{ + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: hunk length 0 for hunk at %ld in file '%s'."), + seek, filename); +} + +/* convenience function: set error for missing hunk */ +static void set_error_a_not_found_in_b (GError **error, + const gchar *a, + const gchar *b, + glong b_seek) +{ + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: no section '%s' found in section '%s' starting at %ld."), + a, b, b_seek); +} + +/* convenience function: set error if header is smaller than expected */ +static void set_error_a_header_smaller_than_b (GError **error, + const gchar *a, + guint32 b, guint32 len, + glong a_seek, + const gchar *filename) +{ + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("header length of '%s' smaller than expected (%d < %d) at offset %ld in file '%s'."), + a, b, len, a_seek, filename); +} + + +/* finds next occurence of section @a in section b (@b_seek) starting + at @start_seek +*/ +/* Return value: + -1 and cts->error not set: section @a could not be found + -1 and cts->error set: some error occured + >=0: start of next occurence of section @a +*/ +static glong find_next_a_in_b (FContents *cts, + const gchar *a, + glong b_seek, glong start_seek) +{ + glong b_len; + glong offset, len; + + g_return_val_if_fail (a, -1); + g_return_val_if_fail (cts, -1); + g_return_val_if_fail (strlen (a) == 4, -1); + g_return_val_if_fail (b_seek>=0, -1); + g_return_val_if_fail (start_seek >= b_seek, -1); + +/* printf ("%s: b_seek: %lx, start_seek: %lx\n", a, b_seek, start_seek); */ + + b_len = get32lint (cts, b_seek+8); + if (cts->error) return -1; + + offset = start_seek - b_seek; + do + { /* skip headers inside the b hunk (b_len) until we find header + @a */ + len = get32lint (cts, b_seek+offset+4); + if (cts->error) return -1; + if (len == 0) + { /* This needs to be checked, otherwise we might hang */ + set_error_zero_length_hunk (&cts->error, b_seek+offset, + cts->filename); + return -1; + } + offset += len; +/* printf ("offset: %lx, b_len: %lx, bseek+offset: %lx\n", */ +/* offset, b_len, b_seek+offset); */ + } while ((offset < b_len-4) && + !check_header_seek (cts, a, b_seek+offset)); + if (cts->error) return -1; + + if (offset >= b_len) return -1; + +/* printf ("%s found at %lx\n", a, b_seek+offset); */ + + return b_seek+offset; +} + + + + +/* return the position of mhsd with type @type */ +/* Return value: + -1 if mhsd cannot be found. cts->error will not be set + + 0 and cts->error is set if some other error occurs. + Since the mhsd can never be at position 0 (a mhbd must be there), + a return value of 0 always indicates an error. +*/ +static glong find_mhsd (FContents *cts, guint32 type) +{ + guint32 i, len, mhsd_num; + glong seek; + + len = get32lint (cts, 4); + if (cts->error) return 0; + /* all the headers I know are 0x68 long -- if this one is longer + we can could simply ignore the additional information */ + /* Since 'we' (parse_fimp()) only need data from the first 32 + bytes, don't complain unless it's smaller than that */ + if (len < 32) + { + g_set_error (&cts->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB ('%s'): header length of mhsd hunk smaller than expected (%d<32). Aborting."), + cts->filename, len); + return FALSE; + } + + mhsd_num = get32lint (cts, 20); + if (cts->error) return 0; + + seek = 0; + for (i=0; ierror) + { /* set error */ + g_set_error (&cts->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB '%s' corrupt: mhsd expected at %ld."), + cts->filename, seek); + } + return 0; + } + len = get32lint (cts, seek+8); + if (cts->error) return 0; + + mhsd_type = get32lint (cts, seek+12); + if (cts->error) return 0; + + if (mhsd_type == type) return seek; + } + return -1; +} + + +/* sort in reverse order */ +static gint pos_comp (gconstpointer a, gconstpointer b) +{ + const PosEntry *pa = (const PosEntry*)a; + const PosEntry *pb = (const PosEntry*)b; + + return pb->track_pos - pa->track_pos; +} + + +/* Read and process the mhip at @seek. Return a pointer to the next + possible mhip. */ +/* Return value: -1 if no mhip is present at @seek */ +static glong get_mhip (FImport *fimp, glong mhip_seek) +{ + gboolean first_entry = TRUE; + FContents *cts; + guint32 mhip_hlen, mhip_len, mhod_num, mhod_seek; + gint32 i; + gint32 mhod_type; + guint32 trackid; + + + g_return_val_if_fail (fimp, -1); + + cts = fimp->fcontents; + + if (!check_header_seek (cts, "mhip", mhip_seek)) + { + CHECK_ERROR (fimp, -1); + return -1; + } + + mhip_hlen = get32lint (cts, mhip_seek+4); + CHECK_ERROR (fimp, -1); + + if (mhip_hlen < 36) + { + set_error_a_header_smaller_than_b (&fimp->error, + "mhip", + mhip_hlen, 36, + mhip_seek, cts->filename); + return -1; + } + + /* Check if entire mhip header can be read -- that way we won't + have to check for read errors every time we access a single + byte */ + + check_seek (cts, mhip_seek, mhip_hlen); + CHECK_ERROR (fimp, -1); + + mhip_len = get32lint (cts, mhip_seek+8); + mhod_num = get32lint (cts, mhip_seek+12); + trackid = get32lint(cts, mhip_seek+24); + + mhod_seek = mhip_seek + mhip_hlen; + + /* the mhod that follows gives us the position in the + playlist (type 100). Just for flexibility, we scan all + following mhods and pick the type 100 */ + for (i=0; itrackid = trackid; + entry->track_pos = mhod.data.track_pos; + fimp->pos_glist = g_list_prepend (fimp->pos_glist, entry); + /* don't call this section more than once (it never + should happen except in the case of corrupted + iTunesDBs...) */ + first_entry = FALSE; + } + } + else + { + if (mhod_len == -1) + { + g_warning (_("Number of MHODs in mhip at %ld inconsistent in file '%s'."), + mhip_seek, cts->filename); + break; + } + } + mhod_seek += mhod_len; + } + + /* Up to iTunesd V4.7 or so the mhip_len was set incorrectly + (mhip_len == mhip_hlen). In that case we need to find the seek + to the next mhip by going through all mhods. + */ + if ((mhip_len == mhip_hlen) && (mhod_num > 0)) + return mhod_seek; + else + return mhip_seek+mhip_len; +} + + + + +/* Get a playlist. Returns the position where the next playlist should + be. On error -1 is returned and fimp->error is set + appropriately. */ +/* get_mhyp */ +static glong get_playlist (FImport *fimp, guint mhsd_type, glong mhyp_seek) +{ + guint32 i, mhipnum, mhod_num; + glong nextseek, mhod_seek, mhip_seek; + guint32 header_len; + Itdb_Playlist *plitem = NULL; + FContents *cts; + GList *gl; + +#if ITUNESDB_DEBUG + fprintf(stderr, "mhyp seek: %x\n", (int)mhyp_seek); +#endif + g_return_val_if_fail (fimp, -1); + g_return_val_if_fail (fimp->idtree, -1); + g_return_val_if_fail (fimp->pos_glist == NULL, -1); + + cts = fimp->fcontents; + + if (!check_header_seek (cts, "mhyp", mhyp_seek)) + { + if (cts->error) + g_propagate_error (&fimp->error, cts->error); + return -1; + } + header_len = get32lint (cts, mhyp_seek+4); /* length of header */ + CHECK_ERROR (fimp, -1); + + if (header_len < 48) + { + set_error_a_header_smaller_than_b (&fimp->error, + "mhyp", + header_len, 48, + mhyp_seek, cts->filename); + return -1; + } + + /* Check if entire mhyp can be read -- that way we won't have to + * check for read errors every time we access a single byte */ + + check_seek (cts, mhyp_seek, header_len); + CHECK_ERROR (fimp, -1); + + nextseek = mhyp_seek + get32lint (cts, mhyp_seek+8);/* possible begin of next PL */ + mhod_num = get32lint (cts, mhyp_seek+12); /* number of MHODs we expect */ + mhipnum = get32lint (cts, mhyp_seek+16); /* number of tracks + (mhips) in playlist */ + + plitem = itdb_playlist_new (NULL, FALSE); + + plitem->num = mhipnum; + /* 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 = get8int (cts, mhyp_seek+20); + plitem->flag1 = get8int (cts, mhyp_seek+21); + plitem->flag2 = get8int (cts, mhyp_seek+22); + plitem->flag3 = get8int (cts, mhyp_seek+23); + plitem->timestamp = get32lint (cts, mhyp_seek+24); + plitem->timestamp = device_time_mac_to_time_t (fimp->itdb->device, plitem->timestamp); + plitem->id = get64lint (cts, mhyp_seek+28); +/* plitem->mhodcount = get32lint (cts, mhyp_seek+36); */ +/* plitem->libmhodcount = get16lint (cts, mhyp_seek+40);*/ + plitem->podcastflag = get16lint (cts, mhyp_seek+42); + plitem->sortorder = get32lint (cts, mhyp_seek+44); + if (header_len >= 0x6C) { + plitem->priv->mhsd5_type = get16lint (cts, mhyp_seek+0x50); + } + + mhod_seek = mhyp_seek + header_len; + + for (i=0; i < mhod_num; ++i) + { + gint32 type; + MHODData mhod; + + type = get_mhod_type (cts, mhod_seek, &header_len); + CHECK_ERROR (fimp, -1); + if (header_len != -1) + { + switch ((enum MHOD_ID)type) + { + case MHOD_ID_PLAYLIST: + /* here we could do something about the playlist settings */ + break; + case MHOD_ID_TITLE: + mhod = get_mhod (fimp, mhod_seek, &header_len); + CHECK_ERROR (fimp, -1); + if (mhod.valid && mhod.data.string) + { + /* sometimes there seem to be two mhod TITLE headers */ + g_free (plitem->name); + plitem->name = mhod.data.string; + mhod.valid = FALSE; + } + break; + case MHOD_ID_SPLPREF: + mhod = get_mhod (fimp, mhod_seek, &header_len); + CHECK_ERROR (fimp, -1); + if (mhod.valid && mhod.data.splpref) + { + plitem->is_spl = TRUE; + memcpy (&plitem->splpref, mhod.data.splpref, + sizeof (Itdb_SPLPref)); + g_free (mhod.data.splpref); + mhod.valid = FALSE; + } + break; + case MHOD_ID_SPLRULES: + mhod = get_mhod (fimp, mhod_seek, &header_len); + CHECK_ERROR (fimp, -1); + if (mhod.valid && mhod.data.splrules) + { + plitem->is_spl = TRUE; + memcpy (&plitem->splrules, mhod.data.splrules, + sizeof (Itdb_SPLRules)); + g_free (mhod.data.splrules); + mhod.valid = FALSE; + } + break; + case MHOD_ID_PATH: + case MHOD_ID_ALBUM: + case MHOD_ID_ARTIST: + case MHOD_ID_GENRE: + case MHOD_ID_FILETYPE: + case MHOD_ID_COMMENT: + case MHOD_ID_CATEGORY: + case MHOD_ID_COMPOSER: + case MHOD_ID_GROUPING: + case MHOD_ID_DESCRIPTION: + case MHOD_ID_PODCASTURL: + case MHOD_ID_PODCASTRSS: + case MHOD_ID_SUBTITLE: + case MHOD_ID_TVSHOW: + case MHOD_ID_TVEPISODE: + case MHOD_ID_TVNETWORK: + case MHOD_ID_ALBUMARTIST: + case MHOD_ID_KEYWORDS: + case MHOD_ID_CHAPTERDATA: + case MHOD_ID_SORT_ARTIST: + case MHOD_ID_SORT_TITLE: + case MHOD_ID_SORT_ALBUM: + case MHOD_ID_SORT_ALBUMARTIST: + case MHOD_ID_SORT_COMPOSER: + case MHOD_ID_SORT_TVSHOW: + case MHOD_ID_ALBUM_ALBUM: + case MHOD_ID_ALBUM_ARTIST: + case MHOD_ID_ALBUM_ARTIST_MHII: + case MHOD_ID_ALBUM_SORT_ARTIST: + case MHOD_ID_LIBPLAYLISTJUMPTABLE: + /* these are not expected here */ + break; + case MHOD_ID_LIBPLAYLISTINDEX: + /* this I don't know how to handle */ + break; + } + mhod_seek += header_len; + } + else + { + g_warning (_("Number of MHODs in mhyp at %ld inconsistent in file '%s'."), + mhyp_seek, cts->filename); + break; + } + } + + if (!plitem->name) + { /* we did not read a valid mhod TITLE header -> */ + /* we simply make up our own name */ + if (itdb_playlist_is_mpl (plitem)) + plitem->name = g_strdup (_("Master-PL")); + else + { + if (itdb_playlist_is_podcasts (plitem)) + plitem->name = g_strdup (_("Podcasts")); + else + plitem->name = g_strdup (_("Playlist")); + } + } + +#if ITUNESDB_DEBUG + fprintf(stderr, "pln: %s(%d Itdb_Tracks) \n", plitem->name, (int)plitem->num); +#endif + + /* add new playlist */ + if (mhsd_type == 5) { + itdb_playlist_add_mhsd5_playlist (fimp->itdb, plitem, -1); + } else { + itdb_playlist_add (fimp->itdb, plitem, -1); + } + + mhip_seek = mhod_seek; + + /* tracks read */ + for (i=0; i < mhipnum; ++i) + { + mhip_seek = get_mhip (fimp, mhip_seek); + if (mhip_seek == -1) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: number of mhip sections inconsistent in mhyp starting at %ld in file '%s'."), + mhyp_seek, cts->filename); + return -1; + } + } + + /* sort in reverse order */ + fimp->pos_glist = g_list_sort (fimp->pos_glist, pos_comp); + for (gl = fimp->pos_glist; gl; gl = g_list_next (gl)) + { + PosEntry* entry = (PosEntry*)gl->data; + Itdb_Track *tr = itdb_track_id_tree_by_id (fimp->idtree, entry->trackid); + if (tr) + { + /* preprend because we sorted in reverse order */ + itdb_playlist_add_track (plitem, tr, 0); + } + else + { + if (plitem->podcastflag == ITDB_PL_FLAG_NORM) + { + g_warning (_("Itdb_Track ID '%d' not found.\n"), entry->trackid); + } + } + g_free(entry); + } + + 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 mhit_seek) +{ + Itdb_Track *track; + gchar *entry_utf8; + gint32 type; + guint32 header_len; + guint32 zip; + struct playcount *playcount; + guint32 i, mhod_nums; + FContents *cts; + glong seek = mhit_seek; + gboolean free_playcount; + +#if ITUNESDB_DEBUG + fprintf(stderr, "get_mhit seek: %x\n", (int)seek); +#endif + + g_return_val_if_fail (fimp, -1); + + cts = fimp->fcontents; + + if (!check_header_seek (cts, "mhit", seek)) + { + if (cts->error) + g_propagate_error (&fimp->error, cts->error); + return -1; + } + + header_len = get32lint (cts, seek+4); + CHECK_ERROR (fimp, -1); + + /* size of the mhit header: For dbversion <= 0x0b (iTunes 4.7 and + earlier), the length is 0x9c. As of dbversion 0x0c and 0x0d + (iTunes 4.7.1 - iTunes 4.9), the size is 0xf4. */ + if (header_len < 0x9c) + { + set_error_a_header_smaller_than_b (&fimp->error, + "mhit", + header_len, 0x9c, + seek, cts->filename); + return -1; + } + + /* Check if entire mhit can be read -- that way we won't have to + * check for read errors every time we access a single byte */ + + check_seek (cts, seek, header_len); + + mhod_nums = get32lint (cts, seek+12); + CHECK_ERROR (fimp, -1); + + + track = itdb_track_new (); + + if (header_len >= 0x9c) + { + guint32 val32; + track->id = get32lint(cts, seek+16); /* iPod ID */ + track->visible = get32lint (cts, seek+20); + track->filetype_marker = get32lint (cts, seek+24); + track->type1 = get8int (cts, seek+28); + track->type2 = get8int (cts, seek+29); + track->compilation = get8int (cts, seek+30); + track->rating = get8int (cts, seek+31); + track->time_modified = get32lint(cts, seek+32); /* time added */ + track->time_modified = device_time_mac_to_time_t (fimp->itdb->device, + track->time_modified); + track->size = get32lint(cts, seek+36); /* file size */ + track->tracklen = get32lint(cts, seek+40); /* time */ + track->track_nr = get32lint(cts, seek+44); /* track number */ + track->tracks = get32lint(cts, seek+48); /* nr of tracks */ + track->year = get32lint(cts, seek+52); /* year */ + track->bitrate = get32lint(cts, seek+56); /* bitrate */ + val32 = get32lint (cts, seek+60); + track->samplerate = val32 >> 16; /* sample rate */ + track->samplerate_low = val32 & 0xffff; /* remaining bits */ + track->volume = get32lint(cts, seek+64); /* volume adjust */ + track->starttime = get32lint (cts, seek+68); + track->stoptime = get32lint (cts, seek+72); + track->soundcheck = get32lint (cts, seek+76);/* soundcheck */ + track->playcount = get32lint (cts, seek+80); /* playcount */ + track->playcount2 = get32lint (cts, seek+84); + track->time_played = get32lint(cts, seek+88);/* last time played */ + track->time_played = device_time_mac_to_time_t (fimp->itdb->device, + track->time_played); + track->cd_nr = get32lint(cts, seek+92); /* CD nr */ + track->cds = get32lint(cts, seek+96); /* CD nr of.. */ + /* Apple Store/Audible User ID (for DRM'ed files only, set to 0 + otherwise). */ + track->drm_userid = get32lint (cts, seek+100); + track->time_added = get32lint(cts, seek+104);/* last mod. time */ + track->time_added = device_time_mac_to_time_t (fimp->itdb->device, + track->time_added); + track->bookmark_time = get32lint (cts, seek+108);/*time bookmarked*/ + track->dbid = get64lint (cts, seek+112); + track->checked = get8int (cts, seek+120); /*Checked/Unchecked: 0/1*/ + /* The rating set by the application, as opposed to the rating + set on the iPod itself */ + track->app_rating = get8int (cts, seek+121); + track->BPM = get16lint (cts, seek+122); + track->artwork_count = get16lint (cts, seek+124); + track->unk126 = get16lint (cts, seek+126); + track->artwork_size = get32lint (cts, seek+128); + track->unk132 = get32lint (cts, seek+132); + track->samplerate2 = get32lfloat (cts, seek+136); + track->time_released = get32lint (cts, seek+140); + track->time_released = device_time_mac_to_time_t (fimp->itdb->device, + track->time_released); + track->unk144 = get16lint (cts, seek+144); + track->explicit_flag = get16lint (cts, seek+146); + track->unk148 = get32lint (cts, seek+148); + track->unk152 = get32lint (cts, seek+152); + } + if (header_len >= 0xf4) + { + track->skipcount = get32lint (cts, seek+156); + track->last_skipped = get32lint (cts, seek+160); + track->last_skipped = device_time_mac_to_time_t (fimp->itdb->device, + track->last_skipped); + track->has_artwork = get8int (cts, seek+164); + track->skip_when_shuffling = get8int (cts, seek+165); + track->remember_playback_position = get8int (cts, seek+166); + track->flag4 = get8int (cts, seek+167); + track->dbid2 = get64lint (cts, seek+168); + track->lyrics_flag = get8int (cts, seek+176); + track->movie_flag = get8int (cts, seek+177); + track->mark_unplayed = get8int (cts, seek+178); + track->unk179 = get8int (cts, seek+179); + track->unk180 = get32lint (cts, seek+180); + track->pregap = get32lint (cts, seek+184); + track->samplecount = get64lint (cts, seek+188); + track->unk196 = get32lint (cts, seek+196); + track->postgap = get32lint (cts, seek+200); + track->unk204 = get32lint (cts, seek+204); + track->mediatype = get32lint (cts, seek+208); + track->season_nr = get32lint (cts, seek+212); + track->episode_nr = get32lint (cts, seek+216); + track->unk220 = get32lint (cts, seek+220); + track->unk224 = get32lint (cts, seek+224); + track->unk228 = get32lint (cts, seek+228); + track->unk232 = get32lint (cts, seek+232); + track->unk236 = get32lint (cts, seek+236); + track->unk240 = get32lint (cts, seek+240); + } + if (header_len >= 0x148) + { + track->unk244 = get32lint (cts, seek+244); + track->gapless_data = get32lint (cts, seek+248); + track->unk252 = get32lint (cts, seek+252); + track->gapless_track_flag = get16lint (cts, seek+256); + track->gapless_album_flag = get16lint (cts, seek+258); + } + if (header_len >= 0x184) + { + track->mhii_link = get32lint (cts, seek+352); + } + + track->transferred = TRUE; /* track is on iPod! */ + + seek += get32lint (cts, seek+4); /* 1st mhod starts here! */ + CHECK_ERROR (fimp, -1); + + for (i=0; ititle = entry_utf8; + break; + case MHOD_ID_PATH: + track->ipod_path = entry_utf8; + break; + case MHOD_ID_ALBUM: + track->album = entry_utf8; + break; + case MHOD_ID_ARTIST: + track->artist = entry_utf8; + break; + case MHOD_ID_GENRE: + track->genre = entry_utf8; + break; + case MHOD_ID_FILETYPE: + track->filetype = entry_utf8; + break; + case MHOD_ID_COMMENT: + track->comment = entry_utf8; + break; + case MHOD_ID_CATEGORY: + track->category = entry_utf8; + break; + case MHOD_ID_COMPOSER: + track->composer = entry_utf8; + break; + case MHOD_ID_GROUPING: + track->grouping = entry_utf8; + break; + case MHOD_ID_DESCRIPTION: + track->description = entry_utf8; + break; + case MHOD_ID_PODCASTURL: + track->podcasturl = entry_utf8; + break; + case MHOD_ID_PODCASTRSS: + track->podcastrss = entry_utf8; + break; + case MHOD_ID_SUBTITLE: + track->subtitle = entry_utf8; + break; + case MHOD_ID_TVSHOW: + track->tvshow = entry_utf8; + break; + case MHOD_ID_TVEPISODE: + track->tvepisode = entry_utf8; + break; + case MHOD_ID_TVNETWORK: + track->tvnetwork = entry_utf8; + break; + case MHOD_ID_ALBUMARTIST: + track->albumartist = entry_utf8; + break; + case MHOD_ID_KEYWORDS: + track->keywords = entry_utf8; + break; + case MHOD_ID_SORT_ARTIST: + track->sort_artist = entry_utf8; + break; + case MHOD_ID_SORT_TITLE: + track->sort_title = entry_utf8; + break; + case MHOD_ID_SORT_ALBUM: + track->sort_album = entry_utf8; + break; + case MHOD_ID_SORT_ALBUMARTIST: + track->sort_albumartist = entry_utf8; + break; + case MHOD_ID_SORT_COMPOSER: + track->sort_composer = entry_utf8; + break; + case MHOD_ID_SORT_TVSHOW: + track->sort_tvshow = entry_utf8; + break; + case MHOD_ID_SPLPREF: + case MHOD_ID_SPLRULES: + case MHOD_ID_LIBPLAYLISTINDEX: + case MHOD_ID_LIBPLAYLISTJUMPTABLE: + case MHOD_ID_PLAYLIST: + case MHOD_ID_CHAPTERDATA: + case MHOD_ID_ALBUM_ALBUM: + case MHOD_ID_ALBUM_ARTIST: + case MHOD_ID_ALBUM_ARTIST_MHII: + case MHOD_ID_ALBUM_SORT_ARTIST: + g_free (entry_utf8); + break; + } + } + else + { + MHODData mhod; + switch (type) + { + case MHOD_ID_CHAPTERDATA: + mhod = get_mhod (fimp, seek, &zip); + if (mhod.valid && mhod.data.chapterdata) + { + track->chapterdata = mhod.data.chapterdata; + mhod.valid = FALSE; + } + break; + default: +/* + printf ("found mhod type %d at %lx inside mhit starting at %lx\n", + type, seek, mhit_seek);*/ + break; + } + } + seek += zip; + } + + playcount = playcount_take_next (fimp); + free_playcount = TRUE; + if (!playcount && fimp->pcounts2) + { + playcount = g_hash_table_lookup (fimp->pcounts2, &track->dbid); + free_playcount = FALSE; + } + if (playcount) + { + if (playcount->rating != NO_PLAYCOUNT) + { + if (track->rating != playcount->rating) + { + /* backup original rating to app_rating */ + track->app_rating = track->rating; + 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; + if (playcount->playcount != 0) + { /* unmark the 'unplayed' flag */ + track->mark_unplayed = 0x01; + } + track->recent_playcount = playcount->playcount; + + track->skipcount += playcount->skipcount; + track->recent_skipcount = playcount->skipcount; + if (free_playcount) + g_free (playcount); + } + fimp->tracks = g_list_prepend(fimp->tracks, track); + 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 (!check_header_seek (cts, "mhpo", 0)) + { + if (cts->error) + { + g_propagate_error (&fimp->error, cts->error); + return FALSE; + } + fcontents_set_reversed (cts, TRUE); + if (!check_header_seek (cts, "mhpo", 0)) + { + /* cts->error can't be set as already checked above */ + /* 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 ('%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; iitdb->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; + gint i=1; + + 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 */ + while (otgname) + { + FContents *cts = fcontents_read (otgname, &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 (otgname); + if (fimp->error) break; + db[0] = g_strdup_printf ("OTGPlaylistInfo_%d", i); + otgname = itdb_resolve_path (dirname, (const gchar **)db); + g_free (db[0]); + ++i; + } + g_free (dirname); + return TRUE; +} + + +/* Read the tracklist (mhlt). mhsd_seek must point to type 1 mhsd + (this is treated as a programming error) */ +/* Return value: + TRUE: import successful + FALSE: error occured, fimp->error is set */ +static gboolean parse_tracks (FImport *fimp, glong mhsd_seek) +{ + FContents *cts; + GList* gl; + glong mhlt_seek, seek; + guint32 nr_tracks, i; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + g_return_val_if_fail (fimp->fcontents, FALSE); + g_return_val_if_fail (fimp->fcontents->filename, FALSE); + g_return_val_if_fail (mhsd_seek >= 0, FALSE); + + cts = fimp->fcontents; + + g_return_val_if_fail (check_header_seek (cts, "mhsd", mhsd_seek), + FALSE); + + /* The mhlt header should be the next after the mhsd header. In + order to allow slight changes in the format, we skip headers + until we find an mhlt inside the given mhsd */ + + mhlt_seek = find_next_a_in_b (cts, "mhlt", mhsd_seek, mhsd_seek); + CHECK_ERROR (fimp, FALSE); + + if (mhlt_seek == -1) + { + set_error_a_not_found_in_b (&fimp->error, + "mhlt", "mhsd", mhsd_seek); + return FALSE; + } + + /* Now we are at the mhlt */ + nr_tracks = get32lint (cts, mhlt_seek+8); + CHECK_ERROR (fimp, FALSE); + + seek = find_next_a_in_b (cts, "mhit", mhsd_seek, mhlt_seek); + CHECK_ERROR (fimp, FALSE); + /* seek should now point to the first mhit */ + for (i=0; ierror) return FALSE; + if (seek == -1) + { /* this should not be -- issue warning */ + g_warning (_("iTunesDB corrupt: number of tracks (mhit hunks) inconsistent. Trying to continue.\n")); + break; + } + } + for (gl=fimp->tracks; gl; gl=g_list_next(gl)) { + Itdb_Track *track = (Itdb_Track *)gl->data; + itdb_track_add (fimp->itdb, track, 0); + } + return TRUE; +} + + + +/* Read the playlists (mhlp). mhsd_seek must point to type 2 or type 3 + mhsd (this is treated as a programming error) */ +/* Return value: + TRUE: import successful + FALSE: error occured, fimp->error is set */ +static gboolean parse_playlists (FImport *fimp, glong mhsd_seek) +{ + FContents *cts; + glong seek, mhlp_seek; + guint32 nr_playlists, i; + guint mhsd_type; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + g_return_val_if_fail (fimp->fcontents, FALSE); + g_return_val_if_fail (fimp->fcontents->filename, FALSE); + g_return_val_if_fail (mhsd_seek >= 0, FALSE); + + cts = fimp->fcontents; + + g_return_val_if_fail (check_header_seek (cts, "mhsd", mhsd_seek), + FALSE); + mhsd_type = get32lint (cts, mhsd_seek+12); + + /* The mhlp header should be the next after the mhsd header. In + order to allow slight changes in the format, we skip headers + until we find an mhlp inside the given mhsd */ + mhlp_seek = find_next_a_in_b (cts, "mhlp", mhsd_seek, mhsd_seek); + CHECK_ERROR (fimp, FALSE); + + if (mhlp_seek == -1) + { + set_error_a_not_found_in_b (&fimp->error, + "mhlp", "mhsd", mhsd_seek); + return FALSE; + } + /* Now we are at the mhlp */ + + nr_playlists = get32lint (cts, mhlp_seek+8); + CHECK_ERROR (fimp, FALSE); + + /* Create track-id tree for quicker track lookup */ + fimp->idtree = itdb_track_id_tree_create (fimp->itdb); + + seek = find_next_a_in_b (cts, "mhyp", mhsd_seek, mhlp_seek); + CHECK_ERROR (fimp, FALSE); + /* seek should now point to the first mhit */ + for (i=0; ierror) 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; +} + +static gboolean parse_genius_mhsd(FImport *fimp, glong mhsd_seek) +{ + FContents *cts; + guint32 hdrlen, mhsdlen; + gint32 len; + gchar *genius_cuid; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + g_return_val_if_fail (fimp->fcontents, FALSE); + g_return_val_if_fail (fimp->fcontents->filename, FALSE); + g_return_val_if_fail (mhsd_seek >= 0, FALSE); + + cts = fimp->fcontents; + + g_return_val_if_fail (check_header_seek (cts, "mhsd", mhsd_seek), + FALSE); + + hdrlen = get32lint (cts, mhsd_seek+4); + mhsdlen = get32lint (cts, mhsd_seek+8); + + len = mhsdlen - hdrlen; + if (len < 0) { + return FALSE; + } + + if (len != 32) { + g_warning(_("%s: Unexpected length %d for genius_cuid!\n"), __func__, len); + } + + genius_cuid = g_new0(gchar, len+1); + if (!seek_get_n_bytes (cts, genius_cuid, mhsd_seek+hdrlen, len)) { + g_free (genius_cuid); + return FALSE; + } + + fimp->itdb->priv->genius_cuid = genius_cuid; + + return TRUE; +} + +static gboolean looks_like_itunesdb (FImport *fimp) +{ + FContents *cts; + + cts = fimp->fcontents; + + /* Check if it looks like an iTunesDB */ + if (!check_header_seek (cts, "mhbd", 0)) + { + fcontents_set_reversed (cts, TRUE); + if (cts->error) return 0; + if (!check_header_seek (cts, "mhbd", 0)) + { + if (!cts->error) + { /* set error */ + g_set_error (&cts->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Not a iTunesDB: '%s' (missing mhdb header)."), + cts->filename); + } + return FALSE; + } + } + return TRUE; +} + +static gboolean parse_fimp (FImport *fimp, gboolean compressed) +{ + glong seek=0; + FContents *cts; + glong mhsd_1, mhsd_2, mhsd_3, mhsd_5, mhsd_9; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + g_return_val_if_fail (fimp->fcontents, FALSE); + g_return_val_if_fail (fimp->fcontents->filename, FALSE); + + if (!looks_like_itunesdb (fimp)) { + return FALSE; + } + + if (compressed) { + itdb_zlib_check_decompress_fimp (fimp); + } + + cts = fimp->fcontents; + + /* get the positions of the various mhsd */ + /* type 1: track list */ + mhsd_1 = find_mhsd (cts, 1); + CHECK_ERROR (fimp, FALSE); + /* type 2: standard playlist section -- Podcasts playlist will be + just an ordinary playlist */ + mhsd_2 = find_mhsd (cts, 2); + CHECK_ERROR (fimp, FALSE); + /* type 3: playlist section with special version of Podcasts + playlist (optional) */ + mhsd_3 = find_mhsd (cts, 3); + CHECK_ERROR (fimp, FALSE); + + /* read smart playlists */ + mhsd_5 = find_mhsd (cts, 5); + CHECK_ERROR (fimp, FALSE); + + /* read genius cuid mhsd */ + mhsd_9 = find_mhsd (cts, 9); + CHECK_ERROR (fimp, FALSE); + + fimp->itdb->version = get32lint (cts, seek+0x10); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->id = get64lint (cts, seek+0x18); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->priv->platform = get16lint (cts, seek+0x20); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->priv->unk_0x22 = get16lint (cts, seek+0x22); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->priv->id_0x24 = get64lint (cts, seek+0x24); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->priv->lang = get16lint (cts, seek+0x46); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->priv->pid = get64lint (cts, seek+0x48); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->priv->unk_0x50 = get32lint (cts, seek+0x50); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->priv->unk_0x54 = get32lint (cts, seek+0x54); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->tzoffset = get32lint (cts, seek+0x6c); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->priv->audio_language = get16lint (cts, seek+0xA0); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->priv->subtitle_language = get16lint (cts, seek+0xA2); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->priv->unk_0xa4 = get16lint (cts, seek+0xA4); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->priv->unk_0xa6 = get16lint (cts, seek+0xA6); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->priv->unk_0xa8 = get16lint (cts, seek+0xA8); + CHECK_ERROR (fimp, FALSE); + if(fimp->itdb->priv->unk_0xa8 != 0) { + g_warning ("Unknown value for 0xa8 in header: should be 0 for uncompressed, is %d.\n", fimp->itdb->priv->unk_0xa8); + } + + if (mhsd_1 == -1) + { /* Very bad: no type 1 mhsd which should hold the tracklist */ + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB '%s' corrupt: Could not find tracklist (no mhsd type 1 section found)"), + cts->filename); + return FALSE; + } + + /* copy the 'reversed endian flag' */ + if (cts->reversed) { + fimp->itdb->device->byte_order = G_BIG_ENDIAN; + } else { + fimp->itdb->device->byte_order = G_LITTLE_ENDIAN; + } +#if 0 + fimp->itdb->device->endianess_set = TRUE; + fimp->itdb->device->endianess_reversed = cts->reversed; +#endif + + parse_tracks (fimp, mhsd_1); + if (fimp->error) return FALSE; + + if (mhsd_3 != -1) + parse_playlists (fimp, mhsd_3); + else if (mhsd_2 != -1) + parse_playlists (fimp, mhsd_2); + else + { /* Very bad: no type 2 or type 3 mhsd which should hold the + playlists */ + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB '%s' corrupt: Could not find playlists (no mhsd type 2 or type 3 sections found)"), + cts->filename); + return FALSE; + } + + if (mhsd_5 != -1) { + parse_playlists (fimp, mhsd_5); + } + + if (mhsd_9 != -1) { + parse_genius_mhsd (fimp, mhsd_9); + } + + return TRUE; +} + + + +/* Set @error with standard error message */ +static void error_no_itunes_dir (const gchar *mp, GError **error) +{ + gchar *str; + + g_return_if_fail (mp); + g_return_if_fail (error); + + str = g_build_filename (mp, "iPod_Control", "iTunes", NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("iTunes directory not found: '%s' (or similar)."), + str); + g_free (str); +} + +/* Set @error with standard error message */ +static void error_no_music_dir (const gchar *mp, GError **error) +{ + gchar *str; + + g_return_if_fail (mp); + g_return_if_fail (error); + + str = g_build_filename (mp, "iPod_Control", "Music", NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Music directory not found: '%s' (or similar)."), + str); + g_free (str); +} + +#if 0 +/* Set @error with standard error message */ +static void error_no_control_dir (const gchar *mp, GError **error) +{ + gchar *str; + + g_return_if_fail (mp); + g_return_if_fail (error); + + str = g_build_filename (mp, "iPod_Control", NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Control directory not found: '%s' (or similar)."), + str); + g_free (str); +} +#endif + + +static gboolean +itdb_parse_internal (Itdb_iTunesDB *itdb, gboolean compressed, GError **error) +{ + FImport *fimp; + gboolean success = FALSE; + + g_return_val_if_fail (itdb->filename != NULL, FALSE); + + fimp = g_new0 (FImport, 1); + fimp->itdb = itdb; + + fimp->fcontents = fcontents_read (itdb->filename, error); + + if (fimp->fcontents) + { + itdb_hash72_extract_hash_info (fimp->itdb->device, + (guchar *)fimp->fcontents->contents, + fimp->fcontents->length); + if (!playcounts_init (fimp)) + { + g_warning ("Error parsing recent playcounts"); + } + if (parse_fimp (fimp, compressed)) + { + if (read_OTG_playlists (fimp)) + { + success = TRUE; + } + } + } + + if (fimp->error) + g_propagate_error (error, fimp->error); + + itdb_free_fimp (fimp); + + return success; +} + +/** + * itdb_parse: + * @mp: mount point of the iPod (eg "/mnt/ipod") in local encoding + * @error: return location for a #GError or NULL + * + * Parse the Itdb_iTunesDB of the iPod located at @mp + * + * Returns: a newly allocated #Itdb_iTunesDB struct holding the tracks and + * the playlists present on the iPod at @mp, NULL if @mp isn't an iPod mount + * point. If non-NULL, the #Itdb_iTunesDB is to be freed with itdb_free() when + * it's no longer needed + */ +Itdb_iTunesDB *itdb_parse (const gchar *mp, GError **error) +{ + gchar *filename; + Itdb_iTunesDB *itdb = NULL; + gboolean compressed = FALSE; + + filename = itdb_get_itunescdb_path (mp); + if (!filename) { + filename = itdb_get_itunesdb_path (mp); + } else { + compressed = TRUE; + } + if (filename) + { + itdb = itdb_new (); + + if (itdb) + { + gboolean success; + + itdb_set_mountpoint (itdb, mp); + itdb->filename = g_strdup (filename); + success = itdb_parse_internal (itdb, compressed, error); + if (success) + { + /* We don't test the return value of ipod_parse_artwork_db + * since the database content will be consistent even if + * we fail to get the various thumbnails, we ignore the + * error since older ipods don't have thumbnails. + + * FIXME: this probably should go into itdb_parse_file, + * but I don't understand its purpose, and + * ipod_parse_artwork_db needs the mountpoint field from + * the itdb, which may not be available in the other + * function + + * JCS: itdb_parse_file is used to read local repositories + * (usually repositories stored in + * ~/.gtkpod). ipod_parse_artwork_db (and the + * corresponding artbook write function) should probably + * be expanded to look for (write) the required files into + * the same directory as itdb->filename in case + * itdb->mountpoint does not exist. Because several local + * repositories may exist in the same directory, the names + * should be modified by the repository name. + */ + ipod_parse_artwork_db (itdb); + } + else + { + itdb_free (itdb); + itdb = NULL; + } + } + } + else + { + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Couldn't find an iPod database on %s."), + mp); + } + g_free (filename); + return itdb; +} + +/** + * itdb_parse_file: + * @filename: path to a file in iTunesDB format + * @error: return location for a #GError or NULL + * + * Same as itunesdb_parse(), but filename is specified directly. + * + * Returns: a newly allocated #Itdb_iTunesDB struct holding the tracks and + * the playlists present in @filename, NULL if @filename isn't a parsable + * iTunesDB file. If non-NULL, the #Itdb_iTunesDB is to be freed with + * itdb_free() when it's no longer needed + */ +Itdb_iTunesDB *itdb_parse_file (const gchar *filename, GError **error) +{ + Itdb_iTunesDB *itdb; + gboolean success; + + g_return_val_if_fail (filename, NULL); + + itdb = itdb_new (); + itdb->filename = g_strdup (filename); + + success = itdb_parse_internal (itdb, FALSE, error); + if (!success) + { + itdb_free (itdb); + itdb = NULL; + } + + 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); + + if (len != 0) + { + g_return_if_fail (data); + 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 @string without trailing Null to end of @cts. Will always be + * successful because glib terminates when out of memory */ +static void put_string (WContents *cts, gchar *string) +{ + g_return_if_fail (cts); + g_return_if_fail (string); + + put_data (cts, string, strlen(string)); +} + +/* Write 4-byte long @header identifcation taking into account + * possible reversed endianess */ +static void put_header (WContents *cts, gchar *header) +{ + gchar rdata[4]; + gint i, offset, sign; + + + g_return_if_fail (cts); + g_return_if_fail (header); + g_return_if_fail (strlen (header) == 4); + + /* reverse data for write if necessary */ + if (cts->reversed) + { + offset = 3; + sign = -1; + } + else + { + offset = 0; + sign = 1; + } + for (i=0; i<4; ++i) + { + rdata[i] = header[offset + sign*i]; + } + + put_data (cts, rdata, 4); +} + + + +/* ------------------------------------------------------------ + Little Endian + ------------------------------------------------------------ */ + +/* Write 2-byte integer @n to @cts in little endian order. */ +static void raw_put16lint (WContents *cts, guint16 n) +{ + n = GUINT16_TO_LE (n); + put_data (cts, (gchar *)&n, 2); +} + +/* Write 3-byte integer @n to @cts in big endian order. */ +static void raw_put24lint (WContents *cts, guint32 n) +{ + gchar buf[3] ; + buf[2] = (n >> 16) & 0xff ; + buf[1] = (n >> 8) & 0xff ; + buf[0] = (n >> 0) & 0xff ; + put_data (cts, buf, 3); +} + + +/* Write 4-byte integer @n to @cts in little endian order. */ +static void raw_put32lint (WContents *cts, guint32 n) +{ + n = GUINT32_TO_LE (n); + put_data (cts, (gchar *)&n, 4); +} + +/* Write 4 byte floating number */ +static void raw_put32lfloat (WContents *cts, float f) +{ + union + { + guint32 i; + float f; + } flt; + + if (sizeof (float) != 4) + { + raw_put32lint (cts, 0); + g_return_if_reached (); + } + + flt.f = f; + raw_put32lint (cts, flt.i); +} + + +/* Write 4-byte integer @n to @cts at position @seek in little + endian order. */ +static void raw_put32lint_seek (WContents *cts, guint32 n, gulong seek) +{ + n = GUINT32_TO_LE (n); + put_data_seek (cts, (gchar *)&n, 4, seek); +} + + +/* Write 8-byte integer @n to @cts in big little order. */ +static void raw_put64lint (WContents *cts, guint64 n) +{ + n = GUINT64_TO_LE (n); + put_data (cts, (gchar *)&n, 8); +} + + +/* ------------------------------------------------------------ + Big Endian + ------------------------------------------------------------ */ + +/* Write 2-byte integer @n to @cts in big endian order. */ +static void raw_put16bint (WContents *cts, guint16 n) +{ + n = GUINT16_TO_BE (n); + put_data (cts, (gchar *)&n, 2); +} + +/* Write 3-byte integer @n to @cts in big endian order. */ +static void raw_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 in big endian order. */ +static void raw_put32bint (WContents *cts, guint32 n) +{ + n = GUINT32_TO_BE (n); + put_data (cts, (gchar *)&n, 4); +} + + +/* Write 4-byte integer @n to @cts at position @seek in big + endian order. */ +static void raw_put32bint_seek (WContents *cts, guint32 n, gulong seek) +{ + n = GUINT32_TO_BE (n); + put_data_seek (cts, (gchar *)&n, 4, seek); +} + + +/* Write 4 byte floating number */ +static void raw_put32bfloat (WContents *cts, float f) +{ + union + { + guint32 i; + float f; + } flt; + + if (sizeof (float) != 4) + { + raw_put32bint (cts, 0); + g_return_if_reached (); + } + + flt.f = f; + raw_put32bint (cts, flt.i); +} + + +/* Write 8-byte integer @n to cts in big endian order. */ +static void raw_put64bint (WContents *cts, guint64 n) +{ + n = GUINT64_TO_BE (n); + put_data (cts, (gchar *)&n, 8); +} + + +/* ------------------------------------------------------------ + Not Endian Dependent + ------------------------------------------------------------ */ + +/* Write 1-byte integer @n to @cts */ +static void put8int (WContents *cts, guint8 n) +{ + put_data (cts, (gchar *)&n, 1); +} + + +/* 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; + } +} + +/* 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; + } +} + + +/* ------------------------------------------------------------ + Reversed Endian Sensitive (little endian) + ------------------------------------------------------------ */ + +static void put16lint (WContents *cts, guint16 n) +{ + if (!cts->reversed) + raw_put16lint (cts, n); + else + raw_put16bint (cts, n); +} + +#if 0 +static void put24lint (WContents *cts, guint32 n) +{ + if (!cts->reversed) + raw_put24lint (cts, n); + else + raw_put24bint (cts, n); +} +#endif +static void put32lint (WContents *cts, guint32 n) +{ + if (!cts->reversed) + raw_put32lint (cts, n); + else + raw_put32bint (cts, n); +} + +static void put32lfloat (WContents *cts, float f) +{ + if (!cts->reversed) + raw_put32lfloat (cts, f); + else + raw_put32bfloat (cts, f); +} + +static void put32lint_seek (WContents *cts, guint32 n, gulong seek) +{ + if (!cts->reversed) + raw_put32lint_seek (cts, n, seek); + else + raw_put32bint_seek (cts, n, seek); +} + + +static void put64lint (WContents *cts, guint64 n) +{ + if (!cts->reversed) + raw_put64lint (cts, n); + else + raw_put64bint (cts, n); +} + +/* ------------------------------------------------------------ + Reversed Endian Sensitive (big endian) + ------------------------------------------------------------ */ +static void put16bint (WContents *cts, guint16 n) +{ + if (cts->reversed) + raw_put16lint (cts, n); + else + raw_put16bint (cts, n); +} + +#if 0 +static void put16bint_seek (WContents *cts, guint16 n, gulong seek) +{ + if (cts->reversed) + raw_put16lint_seek (cts, n, seek); + else + raw_put16bint_seek (cts, n, seek); +} +#endif +static void put24bint (WContents *cts, guint32 n) +{ + if (cts->reversed) + raw_put24lint (cts, n); + else + raw_put24bint (cts, n); +} + +static void put32bint (WContents *cts, guint32 n) +{ + if (cts->reversed) + raw_put32lint (cts, n); + else + raw_put32bint (cts, n); +} +#if 0 +static void put32bfloat (WContents *cts, float f) +{ + if (cts->reversed) + raw_put32lfloat (cts, f); + else + raw_put32bfloat (cts, f); +} +#endif + +static void put32bint_seek (WContents *cts, guint32 n, gulong seek) +{ + if (cts->reversed) + raw_put32lint_seek (cts, n, seek); + else + raw_put32bint_seek (cts, n, seek); +} + +static void put64bint (WContents *cts, guint64 n) +{ + if (cts->reversed) + raw_put64lint (cts, n); + else + raw_put64bint (cts, n); +} + + + +/* Write out the mhbd header. Size will be written later */ +static void mk_mhbd (FExport *fexp, guint32 children) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itdb); + g_return_if_fail (fexp->wcontents); + + cts = fexp->wcontents; + + put_header (cts, "mhbd"); + put32lint (cts, 244); /* header size */ + put32lint (cts, -1); /* size of whole mhdb -- fill in later */ + if (itdb_device_supports_compressed_itunesdb (fexp->itdb->device)) { + /* 2 on iPhone 3.0 and Nano 5G, 1 on iPod Color and iPod Classic + the iPod Classic 3G won't work if set to 2. Tying it to compressed + database support but that's a long shot. */ + put32lint (cts, 2); + } else { + put32lint (cts, 1); + } + + /* 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 + 0x0e: iTunes 5 + 0x0f: iTunes 6 + 0x10: iTunes 6.0.1(?) + 0x11: iTunes 6.0.2 + 0x12 = iTunes 6.0.5. + 0x13 = iTunes 7 + 0x14 = iTunes 7.1 + 0x15 = iTunes 7.2 + 0x19 = iTunes 7.4 + 0x28 = iTunes 8.2.1 + 0x2a = iTunes 9.0.1 + 0x2e = iTunes 9.1 + 0x30 = iTunes 9.2 + Be aware that newer ipods won't work if the library version number is too + old + */ + fexp->itdb->version = 0x30; + put32lint (cts, fexp->itdb->version); + put32lint (cts, children); + put64lint (cts, fexp->itdb->id); + /* 0x20 */ + put16lint (cts, fexp->itdb->priv->platform); /* OS type, 1 = MacOS X, 2 = Windows */ + + /* 0x22 */ + put16lint (cts, fexp->itdb->priv->unk_0x22); /* unknown */ + put64lint (cts, fexp->itdb->priv->id_0x24); /* unkown id */ + put32lint (cts, 0); /* unknown */ + /* 0x30 */ + put16lint (cts, 0); /* set hashing scheme to 0 for now, will be set + * to the appropriate value in + * itdb_device_write_checksum */ + put16_n0 (cts, 10); /* unknown */ + /* 0x46 */ + put16lint (cts, fexp->itdb->priv->lang); /* language (e.g. 'de' for German) */ + put64lint (cts, fexp->itdb->priv->pid); /* library persistent ID */ + /* 0x50 */ + put32lint (cts, fexp->itdb->priv->unk_0x50); /* unknown: seen: 0x05 for nano 3G */ + /* seen: 0x01 for iPod Color */ + put32lint (cts, fexp->itdb->priv->unk_0x54); /* unknown: seen: 0x4d for nano 3G */ + /* seen: 0x0f for iPod Color */ + put32_n0 (cts, 5); /* 20 bytes hash58 */ + put32lint (cts, fexp->itdb->tzoffset); /* timezone offset in seconds */ + /* 0x70 */ + switch (itdb_device_get_checksum_type(fexp->itdb->device)) { + case ITDB_CHECKSUM_HASHAB: + put16lint (cts, 4); /* new on 4th gen iOS devices */ + break; + case ITDB_CHECKSUM_HASH72: + put16lint (cts, 2); + break; + default: + put16lint (cts, 0); + break; + } + put16lint (cts, 0); /* hash72 */ + put32_n0 (cts, 11); /* hash72 */ + /* 0xa0 */ + put16lint (cts, fexp->itdb->priv->audio_language); /* audio_language */ + put16lint (cts, fexp->itdb->priv->subtitle_language); /* subtitle_language */ + put16lint (cts, fexp->itdb->priv->unk_0xa4); /* unknown */ + put16lint (cts, fexp->itdb->priv->unk_0xa6); /* unknown */ + put16lint (cts, fexp->itdb->priv->unk_0xa8); /* unknown */ + put8int (cts, 0); + /* 0xab */ + put8int (cts, 0); /* hashAB */ + put32_n0 (cts, 14); /* hashAB */ + put32_n0 (cts, 4); /* 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); +} + +/* Fill in the length of a short header which are common in the itunesSD */ +static void fix_short_header (WContents *cts, gulong header_seek) +{ + put32lint_seek (cts, cts->pos-header_seek, header_seek+4); +} + +/* 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->wcontents); + + cts = fexp->wcontents; + + put_header (cts, "mhsd"); + 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->wcontents); + + cts = fexp->wcontents; + + put_header (cts, "mhlt"); + 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) +{ + guint32 mac_time; + g_return_if_fail (cts); + g_return_if_fail (track); + + put_header (cts, "mhit"); + put32lint (cts, 0x248);/* header size */ + put32lint (cts, -1); /* size of whole mhit -- fill in later */ + put32lint (cts, -1); /* nr of mhods in this mhit -- later */ + /* +0x10 */ + put32lint (cts, track->id); /* track index number */ + + put32lint (cts, track->visible); + put32lint (cts, track->filetype_marker); + put8int (cts, track->type1); + put8int (cts, track->type2); + put8int (cts, track->compilation); + put8int (cts, track->rating); + /* +0x20 */ + mac_time = device_time_time_t_to_mac (track->itdb->device, track->time_modified); + put32lint (cts, mac_time); /* timestamp */ + put32lint (cts, track->size); /* filesize */ + put32lint (cts, track->tracklen);/* length of track in ms */ + put32lint (cts, track->track_nr);/* track number */ + /* +0x30 */ + put32lint (cts, track->tracks); /* number of tracks */ + put32lint (cts, track->year); /* the year */ + put32lint (cts, track->bitrate); /* bitrate */ + put32lint (cts, (((guint32)track->samplerate)<<16) | + ((guint32)track->samplerate_low)); + /* +0x40 */ + put32lint (cts, track->volume); /* volume adjust */ + put32lint (cts, track->starttime); + put32lint (cts, track->stoptime); + put32lint (cts, track->soundcheck); + /* +0x50 */ + put32lint (cts, track->playcount);/* playcount */ + /* track->playcount2 = track->playcount; */ + put32lint (cts, track->playcount2); /* playcount2, keep original value */ + mac_time = device_time_time_t_to_mac (track->itdb->device, track->time_played); + put32lint (cts, mac_time); /* last time played */ + put32lint (cts, track->cd_nr); /* CD number */ + /* +0x60 */ + put32lint (cts, track->cds); /* number of CDs */ + put32lint (cts, track->drm_userid); + mac_time = device_time_time_t_to_mac (track->itdb->device, track->time_added); + put32lint (cts, mac_time); /* timestamp */ + put32lint (cts, track->bookmark_time); + /* +0x70 */ + put64lint (cts, track->dbid); + if (track->checked) put8int (cts, 1); + else put8int (cts, 0); + put8int (cts, track->app_rating); + put16lint (cts, track->BPM); + put16lint (cts, track->artwork_count); + put16lint (cts, track->unk126); + /* +0x80 */ + put32lint (cts, track->artwork_size); + put32lint (cts, track->unk132); + put32lfloat (cts, track->samplerate2); + mac_time = device_time_time_t_to_mac (track->itdb->device, track->time_released); + put32lint (cts, mac_time); + /* +0x90 */ + put16lint (cts, track->unk144); + put16lint (cts, track->explicit_flag); + put32lint (cts, track->unk148); + put32lint (cts, track->unk152); + /* since iTunesDB version 0x0c */ + put32lint (cts, track->skipcount); + /* +0xA0 */ + mac_time = device_time_time_t_to_mac (track->itdb->device, track->last_skipped); + put32lint (cts, mac_time); + put8int (cts, track->has_artwork); + put8int (cts, track->skip_when_shuffling); + put8int (cts, track->remember_playback_position); + put8int (cts, track->flag4); + put64lint (cts, track->dbid2); + /* +0xB0 */ + put8int (cts, track->lyrics_flag); + put8int (cts, track->movie_flag); + put8int (cts, track->mark_unplayed); + put8int (cts, track->unk179); + put32lint (cts, track->unk180); + put32lint (cts, track->pregap); + put64lint (cts, track->samplecount); + put32lint (cts, track->unk196); + put32lint (cts, track->postgap); + put32lint (cts, track->unk204); + /* +0xD0 */ + put32lint (cts, track->mediatype); + put32lint (cts, track->season_nr); + put32lint (cts, track->episode_nr); + put32lint (cts, track->unk220); + /* +0xE0 */ + put32lint (cts, track->unk224); + put32lint (cts, track->unk228); + put32lint (cts, track->unk232); + put32lint (cts, track->unk236); + /* +0xF0 */ + put32lint (cts, track->unk240); + put32lint (cts, track->unk244); + put32lint (cts, track->gapless_data); + put32lint (cts, track->unk252); + /* +0x100 */ + put16lint (cts, track->gapless_track_flag); + put16lint (cts, track->gapless_album_flag); + put32_n0 (cts, 7); + /* +0x120 */ + put32lint (cts, track->priv->album_id); + put64lint (cts, track->itdb->priv->id_0x24); /* same as mhbd+0x24, purpose unknown */ + put32lint (cts, track->size); /* seems to be filesize again */ + /* +0x130 */ + put32lint (cts, 0); + put64lint (cts, 0x808080808080LL); /* what the heck is this?! */ + put32lint (cts, 0); + /* +0x140 */ + put32_n0 (cts, 2); + if ((track->mediatype & ITDB_MEDIATYPE_EPUB_BOOK) + || (track->mediatype & ITDB_MEDIATYPE_PDF_BOOK)) { + put16lint (cts, 1); + put16lint (cts, 1); + } else { + put32lint (cts, 0); + } + put32_n0 (cts, 5); + /* +0x160 */ + /* mhii_link is needed on fat nanos/ipod classic to get artwork + * in the right sidepane. This matches mhii::song_id in the ArtworkDB */ + put32lint (cts, track->mhii_link); + put32lint (cts, 0); + put32lint (cts, 1); /* unknown */ + put32lint (cts, 0); + /* +0x170 */ + put32_n0 (cts, 28); + /* +0x1E0 */ + put32lint (cts, track->priv->artist_id); + put32_n0 (cts, 4); + /* +0x1F4 */ + put32lint (cts, track->priv->composer_id); /* FIXME: this is another ID for something, composer?! */ + put32_n0 (cts, 20); /* padding */ +} + + +/* 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); +} + +/* Returns: + - track->sort_artist if track->sort_artist is not NULL + + - 'Artist, The' if track->sort_artist is NULL and track->artist of + of type 'The Artist'. + + - NULL if track->sort_artist is NULL and track->artist is not of + type 'The Artist'. + + You must g_free() the returned string */ +static gchar *get_sort_artist (Itdb_Track *track) +{ + g_return_val_if_fail (track, NULL); + + if (track->sort_artist && *track->sort_artist) + { + return g_strdup (track->sort_artist); + } + + if (!(track->artist && *track->artist)) + { + return NULL; + } + + /* check if artist is of type 'The Artist' */ + if (g_ascii_strncasecmp ("The ", track->artist, 4) == 0) + { /* return 'Artist, The', followed by five 0x01 chars + (analogous to iTunes) */ + return g_strdup_printf ("%s, The%c%c%c%c%c", + track->artist+4, + 0x01, 0x01, 0x01, 0x01, 0x01); + } + + return NULL; +} + + + + +static gint mhod52_sort_title (const struct mhod52track *a, const struct mhod52track *b) +{ + return strcmp (a->title, b->title); +} + + +static gint mhod52_sort_album (const struct mhod52track *a, const struct mhod52track *b) +{ + gint result; + + result = strcmp (a->album, b->album); + if (result == 0) + result = a->cd_nr - b->cd_nr; + if (result == 0) + result = a->track_nr - b->track_nr; + if (result == 0) + result = strcmp (a->title, b->title); + return result; +} + +static gint mhod52_sort_artist (struct mhod52track *a, struct mhod52track *b) +{ + gint result; + + result = strcmp (a->artist, b->artist); + if (result == 0) + result = strcmp (a->album, b->album); + if (result == 0) + result = a->cd_nr - b->cd_nr; + if (result == 0) + result = a->track_nr - b->track_nr; + if (result == 0) + result = strcmp (a->title, b->title); + return result; +} + +static gint mhod52_sort_genre (struct mhod52track *a, struct mhod52track *b) +{ + gint result; + + result = strcmp (a->genre, b->genre); + if (result == 0) + result = strcmp (a->artist, b->artist); + if (result == 0) + result = strcmp (a->album, b->album); + if (result == 0) + result = a->cd_nr - b->cd_nr; + if (result == 0) + result = a->track_nr - b->track_nr; + if (result == 0) + result = strcmp (a->title, b->title); + return result; +} + +static gint mhod52_sort_composer (struct mhod52track *a, struct mhod52track *b) +{ + gint result; + + result = strcmp (a->composer, b->composer); + if (result == 0) + result = strcmp (a->album, b->album); + if (result == 0) + result = a->cd_nr - b->cd_nr; + if (result == 0) + result = a->track_nr - b->track_nr; + if (result == 0) + result = strcmp (a->title, b->title); + return result; +} + + +/* Return the first alpha-numeric character in the string + Return upper-case for alpha, return 0 for all numbers */ + +static gunichar2 jump_table_letter (gchar *p) +{ + gunichar chr = 0; + gboolean found_alnum_chars = FALSE; + + g_return_val_if_fail (p != NULL, '0'); + g_return_val_if_fail (g_utf8_validate (p, -1, NULL), '0'); + + while (*p != '\0') { + chr = g_utf8_get_char (p); + if (g_unichar_isalnum (chr)) + { + found_alnum_chars = TRUE; + break; + } + p = g_utf8_find_next_char (p, NULL); + } + + if (!found_alnum_chars) { + /* Return number 0 if no alphanumerics in string */ + return '0'; + } + + if (g_unichar_isalpha(chr)) + { + gunichar2 utf16chr; + gunichar upperchr; + gunichar2 *str; + GError *err = NULL; + + upperchr = g_unichar_toupper (chr); + str = g_ucs4_to_utf16 (&upperchr, 1, NULL, NULL, &err); + if (err != NULL) { + fprintf (stderr, "Error in UCS4 to UTF16 conversion: %s, original unichar: %x, toupper unichar: %x\n", err->message, chr, upperchr); + g_error_free (err); + return '0'; + } + else + { + utf16chr = str[0]; + g_free (str); + return utf16chr; + } + } + + return '0'; /* Return number 0 for any digit */ +} + + +/* Create a new list containing all tracks but with collate keys + instead of the actual strings (title, artist, album, genre, + composer) */ +static GList *mhod52_make_collate_keys (GList *tracks) +{ + gint numtracks = g_list_length (tracks); + GList *gl, *coltracks = NULL; + gint index=0; + + for (gl=tracks; gl; gl=gl->next) + { + gchar *str; + struct mhod52track *ct; + Itdb_Track *tr = gl->data; + g_return_val_if_fail (tr, NULL); + + ct = g_new0 (struct mhod52track, 1); + coltracks = g_list_prepend (coltracks, ct); + + /* album */ + if (tr->sort_album && *tr->sort_album) + { + ct->album = g_utf8_collate_key (tr->sort_album, -1); + ct->letter_album = jump_table_letter (tr->sort_album); + } + else if (tr->album) + { + ct->album = g_utf8_collate_key (tr->album, -1); + ct->letter_album = jump_table_letter (tr->album); + } + else + { + ct->album = g_strdup (""); + ct->letter_album = '0'; + } + + /* title */ + if (tr->sort_title && *tr->sort_title) + { + ct->title = g_utf8_collate_key (tr->sort_title, -1); + ct->letter_title = jump_table_letter (tr->sort_title); + } + else if (tr->title) + { + ct->title = g_utf8_collate_key (tr->title, -1); + ct->letter_title = jump_table_letter (tr->title); + } + else + { + ct->title = g_strdup (""); + ct->letter_title = '0'; + } + + /* artist */ + str = get_sort_artist (tr); + if (str) + { + ct->artist = g_utf8_collate_key (str, -1); + ct->letter_artist = jump_table_letter (str); + g_free (str); + } + else if (tr->artist) + { + ct->artist = g_utf8_collate_key (tr->artist, -1); + ct->letter_artist = jump_table_letter (tr->artist); + } + else + { + ct->artist = g_strdup (""); + ct->letter_artist = '0'; + } + + /* genre */ + if (tr->genre) + { + ct->genre = g_utf8_collate_key (tr->genre, -1); + ct->letter_genre = jump_table_letter (tr->genre); + } + else + { + ct->genre = g_strdup (""); + ct->letter_genre = '0'; + } + + /* composer */ + if (tr->sort_composer && *tr->sort_composer) + { + ct->composer = g_utf8_collate_key (tr->sort_composer, -1); + ct->letter_composer = jump_table_letter (tr->sort_composer); + } + else if (tr->composer) + { + ct->composer = g_utf8_collate_key (tr->composer, -1); + ct->letter_composer = jump_table_letter (tr->composer); + } + else + { + ct->composer = g_strdup (""); + ct->letter_composer = '0'; + } + + ct->track_nr = tr->track_nr; + ct->cd_nr = tr->cd_nr; + ct->numtracks = numtracks; + ct->index = index++; + } + return coltracks; +} + + +/* Free all memory used up by the collate keys */ +static void mhod52_free_collate_keys (GList *coltracks) +{ + GList *gl; + + for (gl=coltracks; gl; gl=gl->next) + { + struct mhod52track *ct = gl->data; + g_return_if_fail (ct); + g_free (ct->album); + g_free (ct->title); + g_free (ct->artist); + g_free (ct->genre); + g_free (ct->composer); + g_free (ct); + } + g_list_free (coltracks); +} + +static void +itdb_chapterdata_build_chapter_blob_internal (WContents *cts, + Itdb_Chapterdata *chapterdata) +{ + gulong atom_len_seek; + gint numchapters; + GList *ch_gl = NULL; + /* printf("[%s] -- inserting into \"chapter\"\n", __func__); */ + + numchapters = g_list_length (chapterdata->chapters); + + put32lint (cts, chapterdata->unk024); /* unknown */ + put32lint (cts, chapterdata->unk028); /* unknown */ + put32lint (cts, chapterdata->unk032); /* unknown */ + atom_len_seek = cts->pos; /* needed to fix length */ + put32bint (cts, -1); /* total length of sean atom, fix later */ + put_header (cts, "sean"); + put32bint (cts, 1); /* unknown */ + put32bint (cts, numchapters+1); /* children */ + put32bint (cts, 0); /* unknown */ + for (ch_gl=chapterdata->chapters; ch_gl; ch_gl=ch_gl->next) + { + gunichar2 *title_utf16; + Itdb_Chapter *chapter = ch_gl->data; + glong len; + title_utf16 = g_utf8_to_utf16 (chapter->chaptertitle, -1, NULL, &len, NULL); + fixup_big_utf16 (title_utf16); + put32bint (cts, 42+2*len); /* total length */ + put_header (cts, "chap"); + put32bint (cts, chapter->startpos); /* should we check if startpos=0 here? */ + put32bint (cts, 1); /* children */ + put32bint (cts, 0); /* unknown */ + put32bint (cts, 22+2*len); /* length */ + put_header (cts, "name"); + put32bint (cts, 1); /* unknown */ + put32_n0 (cts, 2); /* unknown */ + put16bint (cts, len); + put_data (cts, (gchar *)title_utf16, 2*len); + g_free (title_utf16); + } + put32bint (cts, 28); /* size */ + put_header (cts, "hedr"); + put32bint (cts, 1); /* unknown */ + put32bint (cts, 0); /* children */ + put32_n0 (cts, 2); /* unknown */ + put32bint (cts, 1); /* unknown */ + + put32bint_seek (cts, cts->pos-atom_len_seek, atom_len_seek); /* size */ +} + +/* Write out one mhod header. + type: see enum of MHMOD_IDs; + data: utf8 string for text items + position indicator for MHOD_ID_PLAYLIST + Itdb_SPLPref for MHOD_ID_SPLPREF + Itdb_SPLRules for MHOD_ID_SPLRULES */ +static void mk_mhod (FExport *fexp, MHODData *mhod) +{ + WContents *cts = fexp->wcontents; + + g_return_if_fail (cts); + g_return_if_fail (mhod->valid); + + switch ((enum MHOD_ID)mhod->type) + { + case MHOD_ID_TITLE: + case MHOD_ID_PATH: + case MHOD_ID_ALBUM: + case MHOD_ID_ARTIST: + case MHOD_ID_GENRE: + case MHOD_ID_FILETYPE: + case MHOD_ID_COMMENT: + case MHOD_ID_CATEGORY: + case MHOD_ID_COMPOSER: + case MHOD_ID_GROUPING: + case MHOD_ID_DESCRIPTION: + case MHOD_ID_SUBTITLE: + case MHOD_ID_TVSHOW: + case MHOD_ID_TVEPISODE: + case MHOD_ID_TVNETWORK: + case MHOD_ID_ALBUMARTIST: + case MHOD_ID_KEYWORDS: + case MHOD_ID_SORT_ARTIST: + case MHOD_ID_SORT_TITLE: + case MHOD_ID_SORT_ALBUM: + case MHOD_ID_SORT_ALBUMARTIST: + case MHOD_ID_SORT_COMPOSER: + case MHOD_ID_SORT_TVSHOW: + case MHOD_ID_ALBUM_ALBUM: + case MHOD_ID_ALBUM_ARTIST: + case MHOD_ID_ALBUM_SORT_ARTIST: + case MHOD_ID_ALBUM_ARTIST_MHII: + g_return_if_fail (mhod->data.string); + /* normal iTunesDBs seem to take utf16 strings), endian-inversed + iTunesDBs seem to take utf8 strings */ + if (!cts->reversed) + { + /* convert to utf16 */ + glong len; + gunichar2 *entry_utf16 = g_utf8_to_utf16 (mhod->data.string, -1, + NULL, &len, NULL); + fixup_little_utf16 (entry_utf16); + put_header (cts, "mhod"); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, sizeof (gunichar2)*len+40); /* size of header + body */ + put32lint (cts, mhod->type);/* type of the mhod */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + put32lint (cts, 1); /* string type UTF16 */ + put32lint (cts, sizeof (gunichar2)*len); /* size of string */ + put32lint (cts, 1); /* unknown, but is set to 1 */ + put32lint (cts, 0); + put_data (cts, (gchar *)entry_utf16, sizeof (gunichar2)*len);/* the string */ + g_free (entry_utf16); + } + else + { + guint32 len = strlen (mhod->data.string); + put_header (cts, "mhod"); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, len+40); /* size of header + body */ + put32lint (cts, mhod->type);/* type of the mhod */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + put32lint (cts, 2); /* string type UTF 8 */ + put32lint (cts, len); /* size of string */ + put8int (cts, 1); /* unknown */ + put8int (cts, 0); /* unknown */ + put8int (cts, 0); /* unknown */ + put8int (cts, 0); /* unknown */ + put32lint (cts, 0); /* unknown */ + put_string (cts, mhod->data.string);/* the string */ + } + break; + case MHOD_ID_PODCASTURL: + case MHOD_ID_PODCASTRSS: + g_return_if_fail (mhod->data.string); + { + guint32 len = strlen (mhod->data.string); + put_header (cts, "mhod"); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, 24+len); /* size of header + data */ + put32lint (cts, mhod->type);/* type of the mhod */ + put32_n0 (cts, 2); /* unknown */ + put_string (cts, mhod->data.string);/* the string */ + } + break; + case MHOD_ID_PLAYLIST: + put_header (cts, "mhod"); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, 44); /* size of header + body */ + put32lint (cts, mhod->type); /* type of the entry */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + put32lint (cts, mhod->data.track_pos);/* position of track in playlist */ + put32_n0 (cts, 4); /* unknown */ + break; + case MHOD_ID_CHAPTERDATA: + g_return_if_fail (mhod->data.chapterdata); + { + gulong header_seek = cts->pos; /* needed to fix length */ + put_header (cts, "mhod"); /* header */ + put32lint (cts, 24); /* header size */ + put32lint (cts, -1); /* total length, fix later */ + put32lint (cts, mhod->type); /* entry type */ + put32_n0 (cts, 2); /* unknown */ + itdb_chapterdata_build_chapter_blob_internal (cts, mhod->data.chapterdata); + fix_header (cts, header_seek); + } + break; + case MHOD_ID_SPLPREF: + g_return_if_fail (mhod->data.splpref); + put_header (cts, "mhod"); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, 96); /* size of header + body */ + put32lint (cts, mhod->type);/* type of the entry */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + put8int (cts, mhod->data.splpref->liveupdate); + put8int (cts, mhod->data.splpref->checkrules? 1:0); + put8int (cts, mhod->data.splpref->checklimits); + put8int (cts, mhod->data.splpref->limittype); + put8int (cts, mhod->data.splpref->limitsort & 0xff); + put8int (cts, 0); /* unknown */ + put8int (cts, 0); /* unknown */ + put8int (cts, 0); /* unknown */ + put32lint (cts, mhod->data.splpref->limitvalue); + put8int (cts, mhod->data.splpref->matchcheckedonly); + /* for the following see note at definitions of limitsort + types in itunesdb.h */ + put8int (cts, (mhod->data.splpref->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 (mhod->data.splrules); + { + gulong header_seek = cts->pos; /* needed to fix length */ + GList *gl; + gint numrules = g_list_length (mhod->data.splrules->rules); + + put_header (cts, "mhod"); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, -1); /* total length, fix later */ + put32lint (cts, mhod->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_header (cts, "SLst"); /* header */ + put32bint (cts, mhod->data.splrules->unk004); /* unknown*/ + put32bint (cts, numrules); + put32bint (cts, mhod->data.splrules->match_operator); + put32_n0 (cts, 30); /* unknown */ + /* end of header, now follow the rules */ + for (gl=mhod->data.splrules->rules; gl; gl=gl->next) + { + Itdb_SPLRule *splr = gl->data; + ItdbSPLFieldType ft; + glong len; + gunichar2 *entry_utf16; + 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 */ + switch (ft) + { + case ITDB_SPLFT_STRING: + /* write string-type rule */ + len = 0; + entry_utf16 = NULL; + /* splr->string may be NULL */ + if (splr->string) + entry_utf16 = g_utf8_to_utf16 (splr->string, + -1,NULL,&len,NULL); + fixup_big_utf16 (entry_utf16); + put32bint (cts, sizeof (gunichar2)*len); /* length of string */ + put_data (cts, (gchar *)entry_utf16, sizeof (gunichar2)*len); + g_free (entry_utf16); + break; + case ITDB_SPLFT_DATE: + case ITDB_SPLFT_INT: + case ITDB_SPLFT_BOOLEAN: + case ITDB_SPLFT_PLAYLIST: + case ITDB_SPLFT_UNKNOWN: + case ITDB_SPLFT_BINARY_AND: { + guint64 fromvalue; + guint64 tovalue; + + fromvalue = splr->fromvalue; + tovalue = splr->tovalue; + + if (ft == ITDB_SPLFT_DATE) { + ItdbSPLActionType at; + at = itdb_splr_get_action_type (splr); + if ((at == ITDB_SPLAT_RANGE_DATE) || + (at == ITDB_SPLAT_DATE)) + { + Itdb_iTunesDB *itdb = fexp->itdb; + fromvalue = device_time_time_t_to_mac (itdb->device, fromvalue); + tovalue = device_time_time_t_to_mac (itdb->device, tovalue); + } + } + + /* write non-string-type rule */ + put32bint (cts, 0x44); /* length of data */ + /* data */ + put64bint (cts, fromvalue); + put64bint (cts, splr->fromdate); + put64bint (cts, splr->fromunits); + put64bint (cts, 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); + break; + } + } + } + /* insert length of mhod junk */ + fix_header (cts, header_seek); + } + break; + case MHOD_ID_LIBPLAYLISTINDEX: + g_return_if_fail (mhod->data.mhod52coltracks); + g_return_if_fail (mhod->data.mhod52coltracks->data); + { + struct mhod52track *ct = mhod->data.mhod52coltracks->data; + gint numtracks = ct->numtracks; + GList *gl; + gpointer compfunc = NULL; + gunichar2 sortkey = 0; + gunichar2 lastsortkey = 0; + guint32 mhod53index = 0; + struct mhod53_entry *m53 = NULL; + + /* sort list */ + switch (mhod->mhod52sorttype) + { + case MHOD52_SORTTYPE_TITLE: + compfunc = mhod52_sort_title; + break; + case MHOD52_SORTTYPE_ALBUM: + compfunc = mhod52_sort_album; + break; + case MHOD52_SORTTYPE_ARTIST: + compfunc = mhod52_sort_artist; + break; + case MHOD52_SORTTYPE_GENRE: + compfunc = mhod52_sort_genre; + break; + case MHOD52_SORTTYPE_COMPOSER: + compfunc = mhod52_sort_composer; + break; + } + g_return_if_fail (compfunc); + + /* sort the tracks */ + mhod->data.mhod52coltracks = g_list_sort (mhod->data.mhod52coltracks, + compfunc); + /* Write the MHOD */ + put_header (cts, "mhod"); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, 4*numtracks+72); /* size of header + body */ + put32lint (cts, mhod->type); /* type of the mhod */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + put32lint (cts, mhod->mhod52sorttype); /* sort type */ + put32lint (cts, numtracks); /* number of entries */ + put32_n0 (cts, 10); /* unknown */ + for (gl=mhod->data.mhod52coltracks; gl; gl=gl->next) + { + ct = gl->data; + g_return_if_fail (ct); + put32lint (cts, ct->index); + + /* This is for the type 53s */ + switch (mhod->mhod52sorttype) + { + case MHOD52_SORTTYPE_TITLE: + sortkey = ct->letter_title; + break; + case MHOD52_SORTTYPE_ALBUM: + sortkey = ct->letter_album; + break; + case MHOD52_SORTTYPE_ARTIST: + sortkey = ct->letter_artist; + break; + case MHOD52_SORTTYPE_GENRE: + sortkey = ct->letter_genre; + break; + case MHOD52_SORTTYPE_COMPOSER: + sortkey = ct->letter_composer; + break; + } + + if (sortkey != lastsortkey) { + m53 = g_new0 (struct mhod53_entry, 1); + m53->letter = sortkey; + m53->start = mhod53index; + m53->count = 0; + mhod->mhod53_list = g_list_append (mhod->mhod53_list, m53); + lastsortkey = sortkey; + } + mhod53index++; + g_assert (m53 != NULL); + m53->count++; + } + } + break; + case MHOD_ID_LIBPLAYLISTJUMPTABLE: + { + GList *gl; + guint32 numentries; + struct mhod53_entry *m53; + + numentries = g_list_length (mhod->mhod53_list); + put_header (cts, "mhod"); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, 12*numentries+40); /* size of header + body */ + put32lint (cts, mhod->type); /* type of the mhod */ + put32_n0 (cts, 2); /* unknown */ + put32lint (cts, mhod->mhod52sorttype); /* sort type */ + put32lint (cts, numentries); /* number of entries */ + put32_n0 (cts, 2); /* unknown */ + + for (gl=mhod->mhod53_list; gl; gl=gl->next) + { + m53 = gl->data; + put16lint (cts, m53->letter); + put16lint (cts, 0); + put32lint (cts, m53->start); + put32lint (cts, m53->count); + } + } + break; + } +} + +static void mk_mhia (gpointer key, gpointer value, gpointer user_data) +{ + FExport *fexp; + WContents *cts; + Itdb_Track *track; + Itdb_Item_Id *id; + MHODData mhod; + guint mhod_num; + gulong mhia_seek; + + track = (Itdb_Track *)key; + g_return_if_fail (track != NULL); + + id = (Itdb_Item_Id *)value; + g_return_if_fail (id != NULL); + + fexp = (FExport *)user_data; + g_return_if_fail (fexp); + g_return_if_fail (fexp->wcontents); + cts = fexp->wcontents; + mhia_seek = cts->pos; + + put_header (cts, "mhia"); /* header */ + put32lint (cts, 88); /* size of header */ + put32lint (cts, -1); /* total size -> later */ + put32lint (cts, 2); /* number of children mhods */ + put32lint (cts, id->id); /* album id */ + put64lint (cts, id->sql_id); /* id used in the sqlite DB */ + put32lint (cts, 2); /* unknown */ + put32_n0 (cts, 14); /* padding */ + + mhod.valid = TRUE; + mhod_num = 0; + if (track->album && *track->album) { + mhod.type = MHOD_ID_ALBUM_ALBUM; + mhod.data.string = track->album; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + + if (track->albumartist && *track->albumartist) { + mhod.type = MHOD_ID_ALBUM_ARTIST; + mhod.data.string = track->albumartist; + mk_mhod (fexp, &mhod); + ++mhod_num; + } else if (track->artist && *track->artist) { + mhod.type = MHOD_ID_ALBUM_ARTIST; + mhod.data.string = track->artist; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + + if (track->sort_albumartist && *track->sort_albumartist) { + mhod.type = MHOD_ID_ALBUM_SORT_ARTIST; + mhod.data.string = track->sort_albumartist; + mk_mhod (fexp, &mhod); + ++mhod_num; + } else if (track->sort_artist && *track->sort_artist) { + mhod.type = MHOD_ID_ALBUM_SORT_ARTIST; + mhod.data.string = track->sort_artist; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + fix_mhit (cts, mhia_seek, mhod_num); +} + +static void mk_mhla (FExport *fexp) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->wcontents); + g_return_if_fail (fexp->albums); + + cts = fexp->wcontents; + + put_header (cts, "mhla"); /* header */ + put32lint (cts, 92); /* size of header */ + /* albums on iPod */ + put32lint (cts, g_hash_table_size (fexp->albums)); + put32_n0 (cts, 20); /* dummy space */ + g_hash_table_foreach (fexp->albums, mk_mhia, fexp); +} + +/* 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->wcontents); + + cts = fexp->wcontents; + + put_header (cts, "mhlp"); /* header */ + put32lint (cts, 92); /* size of header */ + /* playlists on iPod (including main!) */ + put32lint (cts, -1); /* filled in later */ + put32_n0 (cts, 20); /* dummy space */ +} + +static void mk_mhii (gpointer key, gpointer value, gpointer user_data) +{ + FExport *fexp; + WContents *cts; + Itdb_Track *track; + Itdb_Item_Id *id; + MHODData mhod; + guint mhod_num; + gulong mhii_seek; + + track = (Itdb_Track *)key; + g_return_if_fail (track != NULL); + + id = (Itdb_Item_Id *)value; + g_return_if_fail (id != NULL); + + fexp = (FExport *)user_data; + g_return_if_fail (fexp); + g_return_if_fail (fexp->wcontents); + cts = fexp->wcontents; + mhii_seek = cts->pos; + + put_header (cts, "mhii"); /* header */ + put32lint (cts, 80); /* size of header */ + put32lint (cts, -1); /* total size -> later */ + put32lint (cts, 1); /* number of children mhods */ + put32lint (cts, id->id); /* artist id */ + put64lint (cts, id->sql_id); /* artist SQL id */ + put32lint (cts, 2); /* unknown */ + put32_n0 (cts, 12); /* padding */ + + mhod.valid = TRUE; + mhod_num = 0; + + if (track->artist && *track->artist) { + mhod.type = MHOD_ID_ALBUM_ARTIST_MHII; + mhod.data.string = track->artist; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + + fix_mhit (cts, mhii_seek, mhod_num); +} + +static void mk_mhli (FExport *fexp) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->wcontents); + g_return_if_fail (fexp->artists); + + cts = fexp->wcontents; + + put_header (cts, "mhli"); /* header */ + put32lint (cts, 92); /* size of header */ + /* albums on iPod (including main!) */ + put32lint (cts, g_hash_table_size (fexp->artists)); + put32_n0 (cts, 20); /* dummy space */ + g_hash_table_foreach (fexp->artists, mk_mhii, fexp); +} + +/* 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->wcontents); + g_return_if_fail (pl); + + cts = fexp->wcontents; + + put_header (cts, "mhod"); /* 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 childcount, + guint32 podcastgroupflag, + guint32 podcastgroupid, + guint32 trackid, + guint32 timestamp, + guint32 podcastgroupref) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->wcontents); + + cts = fexp->wcontents; + + put_header (cts, "mhip"); + put32lint (cts, 76); /* 4 */ + put32lint (cts, -1); /* fill in later */ /* 8 */ + put32lint (cts, childcount); /* 12 */ + put32lint (cts, podcastgroupflag); /* 16 */ + put32lint (cts, podcastgroupid); /* 20 */ + put32lint (cts, trackid); /* 24 */ + timestamp = device_time_time_t_to_mac (fexp->itdb->device, timestamp); + put32lint (cts, timestamp); /* 28 */ + put32lint (cts, podcastgroupref); /* 32 */ + put32_n0 (cts, 10); /* 36 */ +} + + +/* Write first mhsd hunk. Return FALSE in case of error and set + * fexp->error */ +static gboolean write_mhsd_tracks (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->wcontents, FALSE); + + cts = fexp->wcontents; + + 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; + MHODData mhod; + + g_return_val_if_fail (track, FALSE); + + mhod.valid = TRUE; + + mk_mhit (cts, track); + if (track->title && *track->title) + { + mhod.type = MHOD_ID_TITLE; + mhod.data.string = track->title; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->artist && *track->artist) + { + mhod.type = MHOD_ID_ARTIST; + mhod.data.string = track->artist; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->album && *track->album) + { + mhod.type = MHOD_ID_ALBUM; + mhod.data.string = track->album; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->filetype && *track->filetype) + { + mhod.type = MHOD_ID_FILETYPE; + mhod.data.string = track->filetype; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->comment && *track->comment) + { + mhod.type = MHOD_ID_COMMENT; + mhod.data.string = track->comment; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->ipod_path && *track->ipod_path) + { + mhod.type = MHOD_ID_PATH; + mhod.data.string = track->ipod_path; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->genre && *track->genre) + { + mhod.type = MHOD_ID_GENRE; + mhod.data.string = track->genre; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->category && *track->category) + { + mhod.type = MHOD_ID_CATEGORY; + mhod.data.string = track->category; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->composer && *track->composer) + { + mhod.type = MHOD_ID_COMPOSER; + mhod.data.string = track->composer; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->grouping && *track->grouping) + { + mhod.type = MHOD_ID_GROUPING; + mhod.data.string = track->grouping; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->description && *track->description) + { + mhod.type = MHOD_ID_DESCRIPTION; + mhod.data.string = track->description; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->subtitle && *track->subtitle) + { + mhod.type = MHOD_ID_SUBTITLE; + mhod.data.string = track->subtitle; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->tvshow && *track->tvshow) + { + mhod.type = MHOD_ID_TVSHOW; + mhod.data.string = track->tvshow; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->tvepisode && *track->tvepisode) + { + mhod.type = MHOD_ID_TVEPISODE; + mhod.data.string = track->tvepisode; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->tvnetwork && *track->tvnetwork) + { + mhod.type = MHOD_ID_TVNETWORK; + mhod.data.string = track->tvnetwork; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->albumartist && *track->albumartist) + { + mhod.type = MHOD_ID_ALBUMARTIST; + mhod.data.string = track->albumartist; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->keywords && *track->keywords) + { + mhod.type = MHOD_ID_KEYWORDS; + mhod.data.string = track->keywords; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->podcasturl && *track->podcasturl) + { + mhod.type = MHOD_ID_PODCASTURL; + mhod.data.string = track->podcasturl; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->podcastrss && *track->podcastrss) + { + mhod.type = MHOD_ID_PODCASTRSS; + mhod.data.string = track->podcastrss; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->sort_artist && *track->sort_artist) + { + mhod.type = MHOD_ID_SORT_ARTIST; + mhod.data.string = track->sort_artist; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->sort_title && *track->sort_title) + { + mhod.type = MHOD_ID_SORT_TITLE; + mhod.data.string = track->sort_title; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->sort_album && *track->sort_album) + { + mhod.type = MHOD_ID_SORT_ALBUM; + mhod.data.string = track->sort_album; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->sort_albumartist && *track->sort_albumartist) + { + mhod.type = MHOD_ID_SORT_ALBUMARTIST; + mhod.data.string = track->sort_albumartist; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->sort_composer && *track->sort_composer) + { + mhod.type = MHOD_ID_SORT_COMPOSER; + mhod.data.string = track->sort_composer; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->sort_tvshow && *track->sort_tvshow) + { + mhod.type = MHOD_ID_SORT_TVSHOW; + mhod.data.string = track->sort_tvshow; + mk_mhod (fexp, &mhod); + ++mhod_num; + } + if (track->chapterdata && track->chapterdata->chapters) + { + mhod.type = MHOD_ID_CHAPTERDATA; + mhod.data.chapterdata = track->chapterdata; + mk_mhod (fexp, &mhod); + ++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; +} + + +/* write out the mhip/mhod pairs for playlist @pl. @mhyp_seek points + to the start of the corresponding mhyp, but is not used yet. */ +static gboolean write_playlist_mhips (FExport *fexp, + Itdb_Playlist *pl, + glong mhyp_seek) +{ + GList *gl; + WContents *cts; + guint32 i=0; + guint32 mhip_num; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->wcontents, FALSE); + g_return_val_if_fail (pl, FALSE); + + cts = fexp->wcontents; + + for (gl=pl->members; gl; gl=gl->next) + { + Itdb_Track *track = gl->data; + glong mhip_seek = cts->pos; + MHODData mhod; + + g_return_val_if_fail (track, FALSE); + + mk_mhip (fexp, 1, 0, 0, track->id, 0, 0); + mhod.valid = TRUE; + mhod.type = MHOD_ID_PLAYLIST; + mhod.data.track_pos = i; + mk_mhod (fexp, &mhod); + /* note: with iTunes 4.9 the mhod is counted as a child to + mhip, so we have put the total length of the mhip and mhod + into the mhip header */ + fix_header (cts, mhip_seek); + ++i; + } + + /* set number of mhips */ + mhip_num = g_list_length (pl->members); + put32lint_seek (cts, mhip_num, mhyp_seek+16); + + return TRUE; +} + + +static void free_memberlist (gpointer data); +static void write_one_podcast_group (gpointer key, gpointer value, gpointer userdata); +static void free_memberlist (gpointer data) +{ + GList **memberlist = data; + if (memberlist) + { + g_list_free (*memberlist); + g_free (memberlist); + } +} +static void write_one_podcast_group (gpointer key, gpointer value, + gpointer userdata) +{ + gchar *album = key; + GList **memberlist = value; + FExport *fexp = userdata; + GList *gl; + WContents *cts; + glong mhip_seek; + guint32 groupid; + MHODData mhod; + + g_return_if_fail (album); + g_return_if_fail (memberlist); + g_return_if_fail (fexp); + g_return_if_fail (fexp->itdb); + g_return_if_fail (fexp->wcontents); + + cts = fexp->wcontents; + mhip_seek = cts->pos; + + groupid = fexp->next_id++; + mk_mhip (fexp, 1, 256, groupid, 0, 0, 0); + + mhod.valid = TRUE; + mhod.type = MHOD_ID_TITLE; + mhod.data.string = album; + mk_mhod (fexp, &mhod); + fix_header (cts, mhip_seek); + + /* write members */ + for (gl=*memberlist; gl; gl=gl->next) + { + Itdb_Track *track = gl->data; + guint32 mhip_id; + + g_return_if_fail (track); + + mhip_seek = cts->pos; + mhip_id = fexp->next_id++; + mk_mhip (fexp, 1, 0, mhip_id, track->id, 0, groupid); + mhod.type = MHOD_ID_PLAYLIST; + mhod.data.track_pos = mhip_id; + mk_mhod (fexp, &mhod); + fix_header (cts, mhip_seek); + } +} + + + +/* write out the mhip/mhod pairs for the podcast playlist + @pl. @mhyp_seek points to the start of the corresponding mhyp, but + is not used yet. */ +static gboolean write_podcast_mhips (FExport *fexp, + Itdb_Playlist *pl, + glong mhyp_seek) +{ + GList *gl; + WContents *cts; + guint32 mhip_num; + GHashTable *album_hash; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->wcontents, FALSE); + g_return_val_if_fail (pl, FALSE); + + cts = fexp->wcontents; + + /* Create a list with all available album names because we have to + group the podcasts according to albums */ + album_hash = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, free_memberlist); + for (gl=pl->members; gl; gl=gl->next) + { + GList **memberlist; + gchar *album; + Itdb_Track *track = gl->data; + g_return_val_if_fail (track, FALSE); + + if (track->album) + album = track->album; + else + album = ""; + + memberlist = g_hash_table_lookup (album_hash, album); + + if (!memberlist) + { + memberlist = g_new0 (GList *, 1); + g_hash_table_insert (album_hash, album, memberlist); + } + *memberlist = g_list_append (*memberlist, track); + } + + g_hash_table_foreach (album_hash, write_one_podcast_group, fexp); + + /* set number of mhips */ + mhip_num = g_list_length (pl->members)+g_hash_table_size (album_hash); + put32lint_seek (cts, mhip_num, mhyp_seek+16); + + g_hash_table_destroy (album_hash); + + return TRUE; +} + +/* Write one type 52 and the corresponding type 53 mhod */ +static void mk_mhod52 (enum MHOD52_SORTTYPE mhod52sorttype, FExport *fexp, + MHODData *mhod) +{ + mhod->mhod52sorttype = mhod52sorttype; + mhod->type = MHOD_ID_LIBPLAYLISTINDEX; + mk_mhod (fexp, mhod); +} + +static void mk_mhod53 (enum MHOD52_SORTTYPE mhod52sorttype, FExport *fexp, + MHODData *mhod) +{ + mhod->type = MHOD_ID_LIBPLAYLISTJUMPTABLE; + mk_mhod (fexp, mhod); + g_list_foreach (mhod->mhod53_list, (GFunc)g_free, NULL); + g_list_free (mhod->mhod53_list); + mhod->mhod53_list = NULL; +} + + +/* corresponds to mk_mhyp */ +/* mhsd_type: 2: write normal playlists + 3: write podcast playlist in special format */ +/* Return FALSE in case of error and set fexp->error */ +static gboolean write_playlist (FExport *fexp, + Itdb_Playlist *pl, guint32 mhsd_type) +{ + gulong mhyp_seek; + WContents *cts; + gboolean result = TRUE; + MHODData mhod; + gint mhodnum; + guint32 mac_time; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->wcontents, FALSE); + g_return_val_if_fail (pl, FALSE); + + cts = fexp->wcontents; + mhyp_seek = cts->pos; + +#if ITUNESDB_DEBUG + fprintf(stderr, "Playlist: %s (%d tracks)\n", pl->name, g_list_length (pl->members)); +#endif + + put_header (cts, "mhyp"); /* header */ + put32lint (cts, 108); /* length */ + put32lint (cts, -1); /* size -> later */ + mhodnum = 2; + if (pl->is_spl) + { /* 2 more SPL MHODs */ + mhodnum += 2; + } + else + { + if ((pl->type == ITDB_PL_TYPE_MPL) && pl->members) + { /* 5 more MHOD 52 lists and 5 more MHOD 53 lists*/ + mhodnum += 10; + } + } + put32lint (cts, mhodnum); /* nr of mhods */ + /* number of tracks in plist */ + put32lint (cts, -1); /* number of mhips -> later */ + put8int (cts, pl->type); /* 1 = main, 0 = visible */ + put8int (cts, pl->flag1); /* unknown */ + put8int (cts, pl->flag2); /* unknown */ + put8int (cts, pl->flag3); /* unknown */ + mac_time = device_time_time_t_to_mac (fexp->itdb->device, pl->timestamp); + put32lint (cts, mac_time); /* some timestamp */ + put64lint (cts, pl->id); /* 64 bit ID */ + put32lint (cts, 0); /* unknown, always 0? */ + put16lint (cts, 1); /* string mhod count (1) */ + put16lint (cts, pl->podcastflag); + put32lint (cts, pl->sortorder); + if (mhsd_type == 5) { + put32_n0 (cts, 8); /* ? */ + /* +0x50 */ + put16lint (cts, pl->priv->mhsd5_type); + put16lint (cts, pl->priv->mhsd5_type); /* same as 0x50 ? */ + /* +0x54 */ + if ((pl->priv->mhsd5_type == ITDB_PLAYLIST_MHSD5_MOVIE_RENTALS) + || (pl->priv->mhsd5_type == ITDB_PLAYLIST_MHSD5_RINGTONES)){ + put32lint (cts, 1); /* unknown, 1 for Movie rentals + Ringtones */ + } else { + put32lint (cts, 0); /* 0 otherwise */ + } + put32_n0 (cts, 5); /* ? */ + + + } else { + put32_n0 (cts, 15); /* ? */ + } + + mhod.valid = TRUE; + mhod.type = MHOD_ID_TITLE; + mhod.data.string = pl->name; + mk_mhod (fexp, &mhod); + mk_long_mhod_id_playlist (fexp, pl); + + if ((pl->type == ITDB_PL_TYPE_MPL) && pl->members) + { /* write out the MHOD 52 and MHOD 53 lists */ + /* We have to sort all tracks five times. To speed this up, + translate the utf8 keys into collate_keys and use the + faster strcmp() for comparison */ + mhod.valid = TRUE; + mhod.data.mhod52coltracks = mhod52_make_collate_keys (pl->members); + mhod.mhod53_list = NULL; + + mk_mhod52 (MHOD52_SORTTYPE_TITLE, fexp, &mhod); + mk_mhod53 (MHOD52_SORTTYPE_TITLE, fexp, &mhod); + mk_mhod52 (MHOD52_SORTTYPE_ARTIST, fexp, &mhod); + mk_mhod53 (MHOD52_SORTTYPE_ARTIST, fexp, &mhod); + mk_mhod52 (MHOD52_SORTTYPE_ALBUM, fexp, &mhod); + mk_mhod53 (MHOD52_SORTTYPE_ALBUM, fexp, &mhod); + mk_mhod52 (MHOD52_SORTTYPE_GENRE, fexp, &mhod); + mk_mhod53 (MHOD52_SORTTYPE_GENRE, fexp, &mhod); + mk_mhod52 (MHOD52_SORTTYPE_COMPOSER, fexp, &mhod); + mk_mhod53 (MHOD52_SORTTYPE_COMPOSER, fexp, &mhod); + + mhod52_free_collate_keys (mhod.data.mhod52coltracks); + } + else if (pl->is_spl) + { /* write the smart rules */ + mhod.type = MHOD_ID_SPLPREF; + mhod.data.splpref = &pl->splpref; + mk_mhod (fexp, &mhod); + + mhod.type = MHOD_ID_SPLRULES; + mhod.data.splrules = &pl->splrules; + mk_mhod (fexp, &mhod); + } + + if (itdb_playlist_is_podcasts(pl) && (mhsd_type == 3)) + { + /* write special podcast playlist */ + result = write_podcast_mhips (fexp, pl, mhyp_seek); + } + else if (mhsd_type == 5) + { + /* set number of mhips to 0 for mhsd_type 5 */ + put32lint_seek (cts, 0, mhyp_seek+16); + } + else + { + /* write standard playlist hard-coded tracks */ + result = write_playlist_mhips (fexp, pl, mhyp_seek); + } + fix_header (cts, mhyp_seek); + return result; +} + + +/* 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_playlists (FExport *fexp, guint32 mhsd_type) +{ + GList *gl; + glong mhsd_seek; + glong mhlp_seek; + WContents *cts; + int plcnt = 0; + GList *playlists; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->wcontents, FALSE); + g_return_val_if_fail ((mhsd_type == 2) || (mhsd_type == 3) || (mhsd_type == 5), FALSE); + + cts = fexp->wcontents; + mhsd_seek = cts->pos; /* get position of mhsd header */ + mk_mhsd (fexp, mhsd_type); /* write header */ + /* write header with nr. of playlists */ + mhlp_seek = cts->pos; + mk_mhlp (fexp); + if (mhsd_type == 5) { + playlists = fexp->itdb->priv->mhsd5_playlists; + } else { + playlists = fexp->itdb->playlists; + } + for (gl=playlists; gl; gl=gl->next) + { + Itdb_Playlist *pl = gl->data; + g_return_val_if_fail (pl, FALSE); + + write_playlist (fexp, pl, mhsd_type); + plcnt++; + if (fexp->error) return FALSE; + } + + put32lint_seek (cts, plcnt, mhlp_seek+8); + + fix_header (cts, mhsd_seek); + return TRUE; +} + +static gboolean write_mhsd_albums (FExport *fexp) +{ + 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->wcontents, FALSE); + + cts = fexp->wcontents; + mhsd_seek = cts->pos; /* get position of mhsd header */ + mk_mhsd (fexp, 4); /* write header */ + mk_mhla (fexp); + fix_header (cts, mhsd_seek); + return TRUE; +} + +static gboolean write_mhsd_artists (FExport *fexp) +{ + 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->wcontents, FALSE); + + cts = fexp->wcontents; + mhsd_seek = cts->pos; /* get position of mhsd header */ + mk_mhsd (fexp, 8); /* write header */ + mk_mhli (fexp); + fix_header (cts, mhsd_seek); + return TRUE; +} + +static gboolean write_mhsd_type6 (FExport *fexp) +{ + 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->wcontents, FALSE); + + cts = fexp->wcontents; + mhsd_seek = cts->pos; /* get position of mhsd header */ + mk_mhsd (fexp, 6); /* write header */ + mk_mhlt (fexp, 0); /* for now, produce an empty set */ + fix_header (cts, mhsd_seek); + return TRUE; +} + +static gboolean write_genius_mhsd (FExport *fexp) +{ + 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->wcontents, FALSE); + + if (!fexp->itdb->priv->genius_cuid) { + return TRUE; + } + cts = fexp->wcontents; + mhsd_seek = cts->pos; /* get position of mhsd header */ + mk_mhsd (fexp, 9); /* write header */ + put_string (cts, fexp->itdb->priv->genius_cuid); + fix_header (cts, mhsd_seek); + return TRUE; +} + +static gboolean write_mhsd_type10 (FExport *fexp) +{ + 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->wcontents, FALSE); + + cts = fexp->wcontents; + mhsd_seek = cts->pos; /* get position of mhsd header */ + mk_mhsd (fexp, 10); /* write header */ + mk_mhlt (fexp, 0); /* for now, produce an empty set */ + 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) +{ + g_return_val_if_fail (cts, FALSE); + g_return_val_if_fail (cts->filename, FALSE); + + cts->error = NULL; + return g_file_set_contents (cts->filename, cts->contents, + cts->pos, &cts->error); +} + + +/* 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); + } +} + +static gboolean safe_str_equal (gconstpointer v1, gconstpointer v2) +{ + if ((v1 == NULL) || (v2 == NULL)) { + return (v1 == v2); + } else { + return g_str_equal (v1, v2); + } +} + +static guint itdb_album_hash (gconstpointer v) +{ + Itdb_Track *track = (Itdb_Track *)v; + if (track->album != NULL) { + return g_str_hash (track->album); + } else { + return 0; + } + g_assert_not_reached (); +} + +static gboolean itdb_album_equal (gconstpointer v1, gconstpointer v2) +{ + Itdb_Track *track1 = (Itdb_Track *)v1; + Itdb_Track *track2 = (Itdb_Track *)v2; + gboolean same_show; + gboolean same_album; + + /*album is the same if: + * tvshow is the same, and + * album and albumartist are the same, or + * albumartist is null and artist and album are the same + */ + same_show = safe_str_equal (track1->tvshow, track2->tvshow); + if (!same_show) { + return FALSE; + } + + same_album = safe_str_equal (track1->album, track2->album); + if (!same_album) { + return FALSE; + } + + if ((track1->albumartist != NULL) && (track2->albumartist != NULL)) { + return safe_str_equal (track1->albumartist, track2->albumartist); + } else { + return safe_str_equal (track1->artist, track2->artist); + } +} + +static void add_new_id (GHashTable *ids, Itdb_Track *track, guint new_id) +{ + Itdb_Item_Id *id; + + id = g_new0 (Itdb_Item_Id, 1); + id->id = new_id; + id->sql_id = ((guint64)g_random_int () << 32) | ((guint64)g_random_int ()); + + g_hash_table_insert (ids, track, id); +} + +static guint itdb_artist_hash (gconstpointer v) +{ + Itdb_Track *track = (Itdb_Track *)v; + if (track->artist != NULL) { + return g_str_hash (track->artist); + } else { + return 0; + } +} + +static gboolean itdb_artist_equal (gconstpointer v1, gconstpointer v2) +{ + Itdb_Track *track1 = (Itdb_Track *)v1; + Itdb_Track *track2 = (Itdb_Track *)v2; + + return safe_str_equal (track1->artist, track2->artist); +} + +static guint itdb_composer_hash (gconstpointer v) +{ + Itdb_Track *track = (Itdb_Track *)v; + if (track->composer != NULL) { + return g_str_hash (track->composer); + } else { + return 0; + } +} + +static gboolean itdb_composer_equal (gconstpointer v1, gconstpointer v2) +{ + Itdb_Track *track1 = (Itdb_Track *)v1; + Itdb_Track *track2 = (Itdb_Track *)v2; + + return safe_str_equal (track1->composer, track2->composer); +} + +/* - reassign the iPod IDs + - make sure the itdb->tracks are in the same order as the mpl + - assign album IDs to write the MHLA +*/ +static void prepare_itdb_for_write (FExport *fexp) +{ + GList *gl; + GList *pl; + Itdb_iTunesDB *itdb; + Itdb_Playlist *mpl; + Itdb_Playlist *playlist; + guint album_id = 1; + guint artist_id = 1; + guint composer_id = 1; + + g_return_if_fail (fexp); + itdb = fexp->itdb; + g_return_if_fail (itdb); + + /* Arrange itdb->tracks in the same order as mpl->members + (otherwise On-The-Go Playlists will not show the correct + content. Unfortunately we cannot just copy mpl->members to + itdb->tracks because podcasts do not show up in the MPL. */ + + mpl = itdb_playlist_mpl (itdb); + g_return_if_fail (mpl); + + for (gl=g_list_last(mpl->members); gl; gl=gl->prev) + { + GList *link; + Itdb_Track *track = gl->data; + g_return_if_fail (track); + link = g_list_find (itdb->tracks, track); + g_return_if_fail (link); + + /* move this track to the beginning of itdb_tracks */ + itdb->tracks = g_list_delete_link (itdb->tracks, link); + itdb->tracks = g_list_prepend (itdb->tracks, track); + } + + fexp->next_id = FIRST_IPOD_ID; + + g_assert (fexp->albums == NULL); + fexp->albums = g_hash_table_new_full (itdb_album_hash, itdb_album_equal, + NULL, g_free); + + g_assert (fexp->artists == NULL); + fexp->artists = g_hash_table_new_full (itdb_artist_hash, itdb_artist_equal, + NULL, g_free); + + g_assert (fexp->composers == NULL); + fexp->composers = g_hash_table_new_full (itdb_composer_hash, itdb_composer_equal, + NULL, g_free); + + /* assign unique IDs and create sort keys */ + for (gl=itdb->tracks; gl; gl=gl->next) + { + Itdb_Track *track = gl->data; + Itdb_Item_Id *id; + + g_return_if_fail (track); + track->id = fexp->next_id++; + + if (track->album != NULL) { + /* album ids are used when writing the mhla header */ + id = g_hash_table_lookup (fexp->albums, track); + if (id != NULL) { + track->priv->album_id = id->id; + } else { + add_new_id (fexp->albums, track, album_id); + track->priv->album_id = album_id; + album_id++; + } + } + + if (track->artist != NULL) { + /* artist ids are used when writing the mhli header */ + id = g_hash_table_lookup (fexp->artists, track); + if (id != NULL) { + track->priv->artist_id = id->id; + } else { + add_new_id (fexp->artists, track, artist_id); + track->priv->artist_id = artist_id; + artist_id++; + } + } + + if (track->composer != NULL) { + id = g_hash_table_lookup (fexp->composers, track); + if (id != NULL) { + track->priv->composer_id = id->id; + } else { + add_new_id (fexp->composers, track, composer_id); + track->priv->composer_id = composer_id; + composer_id++; + } + } + } + + /* Make sure playlist->num is correct */ + for (pl = itdb->playlists; pl; pl = pl->next) { + playlist = pl->data; + g_return_if_fail (playlist); + playlist->num = itdb_playlist_tracks_number (playlist); + } +} + +static gboolean itdb_write_file_internal (Itdb_iTunesDB *itdb, + const gchar *filename, + GError **error) +{ + FExport *fexp; + gulong mhbd_seek = 0; + WContents *cts; + gboolean result = TRUE; + guint32 num_mhsds; + + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (itdb->device, FALSE); + g_return_val_if_fail (filename || itdb->filename, FALSE); + + if (!filename) filename = itdb->filename; + + /* set endianess flag */ + if (!itdb->device->byte_order) + itdb_device_autodetect_endianess (itdb->device); + + fexp = g_new0 (FExport, 1); + fexp->itdb = itdb; + fexp->wcontents = wcontents_new (filename); + cts = fexp->wcontents; + + cts->reversed = (itdb->device->byte_order == G_BIG_ENDIAN); + + prepare_itdb_for_write (fexp); + +#if HAVE_GDKPIXBUF + /* only write ArtworkDB if we deal with an iPod + FIXME: figure out a way to store the artwork data when storing + to local directories. At the moment it's the application's task + to handle this. */ + /* The ArtworkDB must be written after the call to + * prepare_itdb_for_write since it needs Itdb_Track::id to be set + * to its final value to write properly on nano video/ipod classics + */ + if (itdb_device_supports_artwork (itdb->device)) { + ipod_write_artwork_db (itdb); + } +#endif + + /* default mhsd count */ + num_mhsds = 8; /* eight mhsds */ + + /* if genius_cuid present, we have one more */ + if (fexp->itdb->priv->genius_cuid) { + num_mhsds++; + } + + mk_mhbd (fexp, num_mhsds); + + /* write tracklist (mhsd type 1) */ + if (!fexp->error && !write_mhsd_tracks (fexp)) { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Error writing list of tracks (mhsd type 1)")); + goto err; + } + + /* write special podcast version mhsd (mhsd type 3) */ + if (!fexp->error && !write_mhsd_playlists (fexp, 3)) { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Error writing special podcast playlists (mhsd type 3)")); + goto err; + } + /* write standard playlist mhsd (mhsd type 2) */ + if (!fexp->error && !write_mhsd_playlists (fexp, 2)) { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Error writing standard playlists (mhsd type 2)")); + goto err; + } + + /* write albums (mhsd type 4) */ + if (!write_mhsd_albums (fexp)) { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Error writing list of albums (mhsd type 4)")); + goto err; + } + /* write artists (mhsd type 8) */ + if (!fexp->error && !write_mhsd_artists (fexp)) { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Error writing list of artists (mhsd type 8)")); + goto err; + } + + /* write empty mhsd type 6, whatever it is */ + if (!fexp->error && !write_mhsd_type6 (fexp)) { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Error writing mhsd type 6")); + goto err; + } + + /* write empty mhsd type 10, whatever it is */ + if (!fexp->error && !write_mhsd_type10 (fexp)) { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Error writing mhsd type 10")); + goto err; + } + + /* write mhsd5 playlists */ + if (!fexp->error && !write_mhsd_playlists (fexp, 5)) { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Error writing mhsd5 playlists")); + goto err; + } + + if (fexp->itdb->priv->genius_cuid) { + /* write genius cuid (mhsd type 9) */ + if (!fexp->error && !write_genius_mhsd (fexp)) { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Error writing mhsd type 9")); + goto err; + } + } + + fix_header (cts, mhbd_seek); + + if (itdb_device_supports_compressed_itunesdb (itdb->device)) { + if (!itdb_zlib_check_compress_fexp (fexp)) { + goto err; + } + } + + /* Set checksum (ipods require it starting from Classic and Nano Video) */ + itdb_device_write_checksum (itdb->device, + (unsigned char *)fexp->wcontents->contents, + fexp->wcontents->pos, + &fexp->error); + if (fexp->error) { + goto err; + } + + if (itdb_device_supports_sqlite_db (itdb->device)) { + if (itdb_sqlite_generate_itdbs(fexp) != 0) { + goto err; + } + } + + if (itdb_device_is_shuffle (itdb->device)) { + /* iPod Shuffle uses a simplified database in addition to the + * iTunesDB */ + if (!itdb_shuffle_write (itdb, &fexp->error)) { + goto err; + } + } + + if (!fexp->error) + { + if (wcontents_write (cts)) { + playcounts_reset (itdb_get_mountpoint (itdb)); + } else { + g_propagate_error (&fexp->error, cts->error); + } + } +err: + if (fexp->error) + { + g_propagate_error (error, fexp->error); + result = FALSE; + } + wcontents_free (cts); + if (fexp->albums != NULL) { + g_hash_table_destroy (fexp->albums); + } + if (fexp->artists != NULL) { + g_hash_table_destroy (fexp->artists); + } + 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 */ + itdb_fsync (); + + return result; +} + +/** + * itdb_write_file: + * @itdb: the #Itdb_iTunesDB to save + * @filename: filename to save @itdb to + * @error: return location for a #GError or NULL + * + * Write the content of @itdb to @filename. If @filename is NULL, it attempts + * to write to @itdb->filename. + * + * Returns: TRUE if all went well, FALSE otherwise + */ + +gboolean itdb_write_file (Itdb_iTunesDB *itdb, const gchar *filename, + GError **error) +{ + return itdb_write_file_internal (itdb, filename, error); +} + +/** + * itdb_write: + * @itdb: the #Itdb_iTunesDB to write to disk + * @error: return location for a #GError or NULL + * + * Write out an iTunesDB. It 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. + */ +gboolean itdb_write (Itdb_iTunesDB *itdb, GError **error) +{ + gchar *itunes_filename, *itunes_path; + gboolean result = FALSE; + + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (itdb_get_mountpoint (itdb), FALSE); + + /* Now handling regular iPod or iPhone/iPod Touch */ + + /* First, let's try to write the .ithmb files containing the artwork data + * since this operation modifies the 'artwork_count' and 'artwork_size' + * field in the Itdb_Track contained in the database. + * Errors happening during that operation are considered non fatal since + * they shouldn't corrupt the main database. + */ + itunes_path = itdb_get_itunes_dir (itdb_get_mountpoint (itdb)); + + if(!itunes_path) + { + error_no_itunes_dir (itdb_get_mountpoint (itdb), error); + return FALSE; + } + + if (itdb_device_supports_compressed_itunesdb (itdb->device)) { + itunes_filename = g_build_filename (itunes_path, "iTunesCDB", NULL); + } else { + itunes_filename = g_build_filename (itunes_path, "iTunesDB", NULL); + } + + itdb_start_sync (itdb); + + result = itdb_write_file_internal (itdb, itunes_filename, error); + g_free (itunes_filename); + + if (result && itdb_device_supports_compressed_itunesdb (itdb->device)) { + /* the iPhone gets confused when it has both an iTunesCDB and a non + * empty iTunesDB + */ + itunes_filename = g_build_filename (itunes_path, "iTunesDB", NULL); + g_file_set_contents(itunes_filename, NULL, 0, NULL); + g_free (itunes_filename); + } + + g_free (itunes_path); + + if (result != FALSE) + { + /* Write SysInfo file if it has changed */ + if (itdb->device->sysinfo_changed) + { + itdb_device_write_sysinfo (itdb->device, error); + } + result = itdb_rename_files (itdb_get_mountpoint (itdb), error); + } + + /* make sure all buffers are flushed as some people tend to + disconnect as soon as gtkpod returns */ + itdb_fsync (); + + itdb_stop_sync (itdb); + + return result; +} + +/** + * itdb_start_sync: + * @itdb: the #Itdb_iTunesDB that is being sync'ed + * + * Hints libgpod that a series of file copies/database saves/... is about + * to start. On regular iPods, this does nothing (but is safe to be used), + * but on iPhones and iPod Touch this makes sure the "Sync in progress" screen + * is shown for the whole duration of the writing process. + * + * Calls to itdb_start_sync must be paired with calls to itdb_stop_sync. Nesting + * is allowed. + * + * Returns: TRUE on success, FALSE on error + */ +gboolean itdb_start_sync (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb != NULL, FALSE); + g_return_val_if_fail (itdb->device != NULL, FALSE); + +#ifdef HAVE_LIBIMOBILEDEVICE + if (itdb->device->iphone_sync_context != NULL) { + itdb->device->iphone_sync_nest_level++; + return TRUE; + } + if (itdb_device_is_iphone_family (itdb->device)) { + int sync_status; + sync_status = itdb_iphone_start_sync (itdb->device, + &itdb->device->iphone_sync_context); + if (sync_status == 0) { + return TRUE; + } else { + return FALSE; + } + } +#endif + + return TRUE; +} + +/** + * itdb_stop_sync: + * @itdb: the #Itdb_iTunesDB that is being sync'ed + * + * Hints libgpod that the series of file copies/database saves/... started + * with itdb_start_sync is finished. On regular iPods, this does nothing + * (but is safe to be used). On iPhones and iPod Touch this will hide the + * "Sync in progress" screen. + * + * Calls to itdb_stop_sync must be paired with calls to itdb_start_sync. Nesting + * is allowed, and only the final itdb_stop_sync will actually stop the sync. + * + * Returns: TRUE on success, FALSE on error + */ +gboolean itdb_stop_sync (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb != NULL, FALSE); + g_return_val_if_fail (itdb->device != NULL, FALSE); + +#ifdef HAVE_LIBIMOBILEDEVICE + if (itdb->device->iphone_sync_nest_level) { + itdb->device->iphone_sync_nest_level--; + return TRUE; + } + if (itdb_device_is_iphone_family (itdb->device)) { + int sync_status; + if (itdb->device->iphone_sync_context == NULL) { + g_warning ("Trying to unlock an already unlocked device"); + return FALSE; + } + sync_status = itdb_iphone_stop_sync (itdb->device->iphone_sync_context); + itdb->device->iphone_sync_context = NULL; + if (sync_status != 0) { + return FALSE; + } + } +#endif + return TRUE; +} + +/* 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 +| Part of the gtkpod project. +| +| Based on itunessd.c written by Steve Wahl for gtkpod-0.88: +| +| Copyright 2005 Steve Wahl +| +| 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 . +| +| 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... + +*/ + +/** + * itdb_shuffle_write: + * @itdb: the #Itdb_iTunesDB to write to disk + * @error: return location for a #GError or NULL + * + * 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. @itdb->mountpoint must point to the mount + * point of the iPod, e.g. "/mnt/ipod" and be in local encoding. + * + * Returns: TRUE on success, FALSE on error, in which case @error is + * set accordingly. + */ +gboolean itdb_shuffle_write (Itdb_iTunesDB *itdb, GError **error) +{ + gchar *itunes_filename, *itunes_path; + gboolean result = FALSE; + + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (itdb_get_mountpoint (itdb), FALSE); + + itunes_path = itdb_get_itunes_dir (itdb_get_mountpoint (itdb)); + + if(!itunes_path) + { + gchar *str = g_build_filename (itdb_get_mountpoint (itdb), + "iPod_Control", "iTunes", NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Path not found: '%s' (or similar)."), + 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 (itdb_get_mountpoint (itdb), error); + + /* make sure all buffers are flushed as some people tend to + disconnect as soon as gtkpod returns */ + itdb_fsync (); + + return result; +} + +/* helper function */ +static gboolean haystack (gchar *filetype, gchar **desclist) +{ + gchar **dlp; + if (!filetype || !desclist) return FALSE; + for (dlp=desclist; *dlp; ++dlp) + { + if (strstr (filetype, *dlp)) return TRUE; + } + return FALSE; +} + +/* Converts a standard itunesDB filetype to the one used in itunesSD */ +/* TODO: Find the values for other filetypes */ +static guint convert_filetype (gchar *filetype) +{ + guint32 stype; + /* Type 0x01 Default value */ + /* gchar *mp3_desc[] = {"MPEG", "MP3", "mpeg", "mp3", NULL}; */ + /* Type 0x02 */ + gchar *mp4_desc[] = {"AAC", "MP4", "M4A", "aac", "mp4", "m4a", NULL}; + /* Type 0x04 */ + gchar *wav_desc[] = {"WAV", "wav", NULL}; + + /* Default to mp3 */ + stype=0x01; + + if (haystack (filetype, mp4_desc)) + stype=0x02; + else if (haystack (filetype, wav_desc)) + stype=0x04; + + return stype; +} + +/* Write out the rths header, the iTunesSD track header */ +static gboolean write_rths (WContents *cts, Itdb_Track *track) +{ + gulong rths_seek; + gint padlength; + gchar *path; + + g_return_val_if_fail (cts, FALSE); + g_return_val_if_fail (track, FALSE); + + rths_seek=cts->pos; + + /* Prep the path */ + /* Correct this if the path field changes length */ + path = g_strndup (track->ipod_path, 256); + g_strdelimit (path, ":", '/'); + + put_header (cts, "rths"); + put32lint (cts, -1); /* Length of header to be added later */ + put32lint (cts, track->starttime); /* Start pos in ms */ + put32lint (cts, track->stoptime); /* Stop pos in ms */ + put32lint (cts, track->volume); /* Volume gain */ + put32lint (cts, convert_filetype (track->filetype)); /* Filetype see + convert filetype*/ + /* If the length of this field changes make sure to correct the + g_strndup above */ + put_string (cts, path); /* Path to the song */ + for (padlength = 256-strlen (path); padlength > 0; padlength--) + put8int (cts, 0); + + put32lint (cts, track->bookmark_time); /* Bookmark time */ + /* Don't Skip on shuffle */ + /* This field has the exact opposite value as in the iTunesDB */ + /* 1 when you want to skip, 0 when you don't want to skip. Causes a + * playlist to be skipped if all songs in that playlist have this set */ + put8int (cts, !track->skip_when_shuffling); + put8int (cts, track->remember_playback_position); /* Remember playing pos */ + /* put8int (cts, ); In uninterruptable album */ + put8int (cts, 0); /* Best guess its gapless album */ + put8int (cts, 0); /* Unknown */ + put32lint (cts, track->pregap); /* Pregap */ + put32lint (cts, track->postgap); /* Postgap */ + put32lint (cts, (guint32) track->samplecount); /* Number of Samples */ + put32_n0 (cts, 1); /* Unknown */ + put32lint (cts, track->gapless_data); /* Gapless Data */ + put32_n0 (cts, 1); /* Unknown */ + put32lint (cts, track->priv->album_id); /* Album ID */ + put16lint (cts, track->track_nr); /* Track number */ + put16lint (cts, track->cd_nr); /* Disk number */ + put32_n0 (cts, 2); /* Unknown */ + put64lint (cts, track->dbid); /* Voiceover Filename also dbid*/ + put32lint (cts, track->priv->artist_id); /* Artist ID */ + put32_n0 (cts, 8); /* Unknown */ + + fix_short_header (cts, rths_seek); + + g_free (path); + + return TRUE; +} + +/* Write out the hths header, the iTunesSD track list header*/ +static gboolean write_hths (FExport *fexp) +{ + gulong hths_seek, track_seek; + WContents *cts; + GList *gl; + guint32 trackcnt; + guint32 nonstdtrackcnt; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->wcontents, FALSE); + + cts = fexp->wcontents; + hths_seek = cts->pos; + trackcnt = itdb_tracks_number(fexp->itdb); + nonstdtrackcnt = 0; /* Counts the number of audiobook and podcast tracks */ + + /* Add Track list offset to bdhs header */ + /* If the bdhs header changes make sure the seek value is still correct */ + put32lint_seek(cts, cts->pos, 36); + + put_header (cts, "hths"); + put32lint (cts, -1); /* Length of header to be added later */ + put32lint (cts, trackcnt); /* Number of tracks */ + put32_n0 (cts, 2); /* Unknown */ + track_seek=cts->pos; /* Put the offset of the first track here */ + put32_n0 (cts, trackcnt); /* The offset for each track to be added later */ + + fix_short_header (cts, hths_seek); + + /* Add each tracks header */ + for(gl=fexp->itdb->tracks;gl;gl=gl->next) + { + Itdb_Track *track = gl->data; + + /* Add this tracks offset then add the track */ + put32lint_seek(cts, cts->pos, track_seek); + g_return_val_if_fail (write_rths(cts, track), FALSE); + + if ((track->mediatype & ITDB_MEDIATYPE_AUDIOBOOK) || + (track->mediatype & ITDB_MEDIATYPE_PODCAST)) + nonstdtrackcnt++; + + /* Go to the offset for the next track */ + track_seek += 4; + } + /* Add the number of nonpodcasts to bdhs header */ + /* If the bdhs header changes make sure the seek value is still correct */ + put32lint_seek(cts, trackcnt-nonstdtrackcnt, 32); + return TRUE; +} + +/* Write out the lphs header, the iTunesSD playlist header */ +static gboolean write_lphs (WContents *cts, Itdb_Playlist *pl) +{ + gulong lphs_seek; + GList *tl, *tracks, *current_track; + Itdb_Track *tr, *ctr; + guint64 id; + guint32 stype; + guint32 tracknum; + guint32 nonstdtrackcnt; + + g_return_val_if_fail (pl, FALSE); + g_return_val_if_fail (pl->itdb, FALSE); + g_return_val_if_fail (cts, FALSE); + + lphs_seek = cts->pos; + tracks = pl->itdb->tracks; + nonstdtrackcnt = 0; /* The number of audiobook and podcast tracks*/ + /* Change the playlist itunesDB type into a itunesSD type */ + /* 1 is for the master playlist + 2 is for a normal playlist + 3 is for the podcast playlist + 4 is for an audiobook playlist */ + if (itdb_playlist_is_mpl (pl)) + stype = 1; + else if (itdb_playlist_is_podcasts (pl)) + stype = 3; + else if (itdb_playlist_is_audiobooks (pl)) + stype = 4; + else /* Everything else we treat as normal */ + stype = 2; + + put_header (cts, "lphs"); + put32lint (cts, -1); /* Length of header to be written later */ + put32lint (cts, pl->num); /* Number of tracks in this playlist */ + put32lint (cts, -1); /* The number of non podcasts or audiobooks to be added later */ + + if (stype == 1) + put32_n0 (cts, 2); /* The voiceover for the master is at 0 */ + else + put64lint (cts, pl->id); /* Voiceover filename also Playlist ID */ + + put32lint (cts, stype); /* Type of playlist */ + put32_n0 (cts, 4); /* Unknown */ + if (tracks) /* It makes no sense to do the following if there is no tracks */ + { + /* Walk the playlist and find and write out the track number + of each track in it */ + for( tl = pl -> members; tl; tl = tl->next) + { + tracknum = 0; + current_track= tracks; + tr = tl->data; + id = tr->dbid; + ctr = current_track->data; + /* Count the number of podcasts and audiobooks for later use */ + if ((tr->mediatype & ITDB_MEDIATYPE_AUDIOBOOK) || + (tr->mediatype & ITDB_MEDIATYPE_PODCAST)) + nonstdtrackcnt++; + + while( id != ctr->dbid) + { + tracknum ++; + current_track = current_track->next; + g_return_val_if_fail (current_track, FALSE); + ctr = current_track->data; + } + put32lint (cts, tracknum); + } + } + + put32lint_seek (cts, pl->num - nonstdtrackcnt, lphs_seek+12); + fix_short_header (cts, lphs_seek); + return TRUE; + +} + +/* Write out the hphs header, the iTunesSD playlist list header */ +static gboolean write_hphs (FExport *fexp) +{ + gulong hphs_seek, playlist_seek; + WContents *cts; + GList *gl; + guint16 playlistcnt; + guint16 podcastscnt; + guint16 mastercnt; + guint16 audiobookscnt; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->wcontents, FALSE); + + cts = fexp->wcontents; + hphs_seek = cts->pos; + playlistcnt = 0; /* Number of nonempty playlists, this may not be the same + as what itdb_playlists_number returns*/ + podcastscnt = 0; /* Number of podcast playlists should be 1 if one exists*/ + mastercnt = 0; /* Number of master playlists should be 1 */ + audiobookscnt = 0; /* Number of audiobook playlists */ + + /* We have to walk the playlist list before we can write the needed counts */ + for (gl = fexp->itdb->playlists; gl; gl = gl->next) { + Itdb_Playlist *pl = gl->data; + + if (!pl->members) { + /* If the playlist has no members skip it */ + continue; + } + /* Otherwise count it and count its type */ + playlistcnt++; + + if (itdb_playlist_is_mpl (pl)){ + mastercnt++; + } + else if (itdb_playlist_is_podcasts (pl)) { + podcastscnt++; + } + else if (itdb_playlist_is_audiobooks (pl)) { + audiobookscnt++; + } + } + + /* Add nonempty playlist count to bdhs */ + put32lint_seek (cts, playlistcnt, 16); + + /* Add the Playlist Header Offset */ + put32lint_seek (cts, cts->pos, 40); + + put_header (cts, "hphs"); + put32lint (cts, -1); /* Length of header to be added later */ + put16lint (cts, playlistcnt); /* Number of playlists */ + put16_n0 (cts, 1); /* Unknown */ + put16lint (cts, playlistcnt-podcastscnt); /* Number of non podcast playlists + there should be a maximum of 1 + podcast playlist. */ + put16lint (cts, mastercnt); /* Number of master playlists there should be a + maximum of 1 master playlist */ + put16lint (cts, playlistcnt-audiobookscnt); /* Number of non audiobook playlists */ + put16_n0 (cts, 1); + + playlist_seek = cts->pos; + put32_n0 (cts, playlistcnt); /* Offsets for each playlist */ + + fix_short_header (cts, hphs_seek); + + for (gl = fexp->itdb->playlists; gl; gl = gl->next) + { + Itdb_Playlist *pl = gl->data; + + if (!pl->members) { + continue; + } + + /* Write this playlist's header offset */ + put32lint_seek (cts, cts->pos, playlist_seek); + + g_return_val_if_fail (write_lphs (cts, pl),FALSE); + + /* Move to the field for the next header */ + playlist_seek += 4; + } + return TRUE; +} + +/* Write out the bdhs header, the main iTunesSD header */ +static gboolean write_bdhs (FExport *fexp) +{ + WContents *cts; + gulong bdhs_seek; + guint32 trackcnt; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->wcontents, FALSE); + + cts = fexp->wcontents; + bdhs_seek = cts->pos; + trackcnt = itdb_tracks_number (fexp->itdb); + + put_header (cts, "bdhs"); + put32lint (cts, 0x02000003); /* Unknown */ + put32lint (cts, -1); /* Length of header to be added later*/ + put32lint (cts, trackcnt); /* Number of tracks */ + put32_n0 (cts, 1); /* Number of nonempty playlists to be written later + in write_hphs see the note below! */ + put32_n0 (cts, 2); /* Unknown */ + /* TODO: Parse the max volume */ + put8int (cts, 0); /* Limit max volume currently ignored and set to off */ + /* TODO: Find another source of the voiceover option */ + put8int (cts, 1); /* Voiceover currently ignored and set to on */ + put16_n0 (cts, 1); /* Unknown */ + + /* NOTE: If the bdhs header changes the offsets of these fields may change + make sure you correct the field offset in their respective tags */ + + put32lint (cts, -1); /* Number of tracks excluding podcasts and audiobooks + added in write_hths */ + put32lint (cts, -1); /* Track Header Offset added in write_hths */ + put32lint (cts, -1); /* Playlist Header Offset added in write_hphs */ + + put32_n0 (cts, 5); /* Unknown */ + + fix_header(cts, bdhs_seek); + + return TRUE; +} + +static gboolean is_shuffle_2g (Itdb_Device *device) +{ + return (itdb_device_get_shadowdb_version (device) == ITDB_SHADOW_DB_V1); +} + +/** + * itdb_shuffle_write_file: + * @itdb: the #Itdb_iTunesDB to write to disk + * @filename: file to write to, cannot be NULL + * @error: return location for a #GError or NULL + * + * Do the actual writing to the iTunesSD + * + * Returns: TRUE on success, FALSE on error, in which case @error is + * set accordingly. + */ +gboolean itdb_shuffle_write_file (Itdb_iTunesDB *itdb, + const gchar *filename, GError **error) +{ + FExport *fexp; + GList *gl; + WContents *cts; + gboolean result = TRUE;; + + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (filename, FALSE); + g_return_val_if_fail (itdb->device, FALSE); + + /* Set endianess flag just in case */ + if (!itdb->device->byte_order) + itdb_device_autodetect_endianess (itdb->device); + + fexp = g_new0 (FExport, 1); + fexp->itdb = itdb; + fexp->wcontents = wcontents_new (filename); + cts = fexp->wcontents; + + cts->reversed = (itdb->device->byte_order == G_BIG_ENDIAN); + + prepare_itdb_for_write (fexp); + + if (is_shuffle_2g (itdb->device)) { + /* Older iTunesSD format */ + 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; + glong pathlen; + + 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->filetype 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) */ + + put24bint (cts, convert_filetype (tr->filetype)); + + put24bint (cts, 0x200); + + path = g_strdup (tr->ipod_path); + /* shuffle uses forward slash separator, not colon */ + g_strdelimit (path, ":", '/'); + path_utf16 = g_utf8_to_utf16 (path, -1, NULL, &pathlen, NULL); + if (pathlen > 261) pathlen = 261; + fixup_little_utf16 (path_utf16); + put_data (cts, (gchar *)path_utf16, sizeof (gunichar2)*pathlen); + /* pad to 522 bytes */ + put16_n0 (cts, 261-pathlen); + g_free(path); + g_free(path_utf16); + + put8int (cts, tr->skip_when_shuffling); /* Is the song used in shuffle mode */ + put8int (cts, tr->remember_playback_position); /* Is the song bookmarkable */ + put8int (cts, 0); + } + }else { + /* Newer iTunesSD format */ + /* Add the main Header */ + write_bdhs(fexp); + + /* Add the Tracks Header */ + if (!write_hths(fexp)){ + g_set_error(&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Error writing list of tracks (hths)")); + goto serr; + } + + /* Add the Playlist Header */ + if(!fexp->error && !write_hphs(fexp)){ + g_set_error(&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Error writing playlists (hphs)")); + goto serr; + } + } + if (!fexp->error) + { + if (!wcontents_write (cts)) + g_propagate_error (&fexp->error, cts->error); + } +serr: + 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 */ + itdb_fsync (); + + return result; +} + +/*------------------------------------------------------------------*\ + * * + * Other file/filename stuff * + * * +\*------------------------------------------------------------------*/ + +/** + * itdb_rename_files: + * @mp: mount point of the iPod + * @error: return location for a #GError or NULL + * + * 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_plc_o[] = {"Play Counts", NULL}; + const gchar *db_otg[] = {"OTGPlaylistInfo", NULL}; + const gchar *db_shu[] = {"iTunesShuffle", NULL}; + const gchar *db_ist[] = {"iTunesStats", NULL}; + gchar *itunesdir; + gchar *plcname_o; + gchar *plcname_n; + gchar *otgname; + gchar *shuname; + gchar *istname; + gboolean result = TRUE; + + g_return_val_if_fail (mp, FALSE); + + itunesdir = itdb_get_itunes_dir (mp); + + if(!itunesdir) + { + error_no_itunes_dir (mp, error); + 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); + shuname = itdb_resolve_path (itunesdir, db_shu); + istname = itdb_resolve_path (itunesdir, db_ist); + + /* 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; + } + } + + /* remove some Shuffle files */ + if (shuname) + { + if (unlink (shuname) == -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)."), + shuname, g_strerror (errno)); + } + result = FALSE; + } + } + + /* remove some Shuffle files */ + if (istname) + { + if (unlink (istname) == -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)."), + istname, g_strerror (errno)); + } + result = FALSE; + } + } + + g_free (plcname_o); + g_free (plcname_n); + g_free (otgname); + g_free (shuname); + g_free (istname); + g_free (itunesdir); + + return result; +} + + +/** + * itdb_filename_fs2ipod: + * @filename: a 'PC-style' filename (eg /iPod_Control/Music/f00/test.mp3) + * + * Convert string from casual PC file name to iPod iTunesDB format + * using ':' instead of G_DIR_SEPARATOR_S (i.e. slashes on Unix-like + * systems). @ipod_file is modified in place. + */ +void itdb_filename_fs2ipod (gchar *ipod_file) +{ + g_strdelimit (ipod_file, G_DIR_SEPARATOR_S, ':'); +} + +/** + * itdb_filename_ipod2fs: + * @ipod_file: a 'PC-style' filename (eg /iPod_Control/Music/f00/test.mp3) + * + * Convert string from from iPod iTunesDB format to casual PC file + * name using G_DIR_SEPARATOR (ie slashes on Unix-like systems) + * instead of ':'. @ipod_file is modified in place. + */ +void itdb_filename_ipod2fs (gchar *ipod_file) +{ + g_strdelimit (ipod_file, ":", G_DIR_SEPARATOR); +} + +/** + * itdb_set_mountpoint: + * @itdb: an #Itdb_iTunesDB + * @mp: new mount point + * + * Sets the mountpoint of @itdb. Always use this function to set the + * mountpoint of an #Itdb_iTunesDB as it will reset the number of + * available /iPod_Control/Music/F.. dirs. It doesn't attempt to parse + * an iPod database that may be present on the iPod at @mp. + * + * Calling this function removes the artwork in the + * #Itdb_iTunesDB database using this #Itdb_Device which was read from the + * iPod.. + * + * Since: 0.1.3 + */ +void itdb_set_mountpoint (Itdb_iTunesDB *itdb, const gchar *mp) +{ + g_return_if_fail (itdb); + g_return_if_fail (itdb->device); + + itdb_device_set_mountpoint (itdb->device, mp); + itdb->device->musicdirs = 0; +} + +/** + * itdb_get_mountpoint: + * @itdb: an #Itdb_iTunesDB + * + * Retrieve a reference to the mountpoint of @itdb + * + * Returns: the @itdb mountpoint, this string shouldn't be freed + * nor modified + * + * Since: 0.4.0 + */ +const gchar *itdb_get_mountpoint (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb, NULL); + g_return_val_if_fail (itdb->device, NULL); + return itdb->device->mountpoint; +} + +/* Retrieve a reference to the mountpoint */ +const gchar *itdb_photodb_get_mountpoint (Itdb_PhotoDB *photodb) +{ + g_return_val_if_fail (photodb, NULL); + g_return_val_if_fail (photodb->device, NULL); + return photodb->device->mountpoint; +} + +/** + * itdb_musicdirs_number: + * @itdb: an #Itdb_iTunesDB + * + * Determine the number of F.. directories in iPod_Control/Music. + * + * If @itdb->musicdirs is already set, simply return the previously + * determined number. Otherwise count the directories first and set + * @itdb->musicdirs. + * + * Returns: max number of directories in iPod_Control/Music + * + * Since: 0.1.3 + */ +gint itdb_musicdirs_number (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb, 0); + g_return_val_if_fail (itdb->device, 0); + + return itdb_device_musicdirs_number (itdb->device); +} + +/** + * itdb_cp_get_dest_filename: + * @track: track to transfer or NULL + * @mountpoint: mountpoint of your iPod or NULL + * @filename: the source file + * @error: return location for a #GError or NULL + * + * Creates a valid filename on the iPod where @filename can be copied. + * + * You must provide either @track or @mountpoint. Providing @track is + * not thread-safe (accesses track->itdb->device and may even write to + * track->itdb->device). Providing @mountpoint is thread-safe but + * slightly slower because the number of music directories is counted + * each time the function is called. + * + * You can use itdb_cp() to copy the track to the iPod or implement + * your own copy function. After the file was copied you have to call + * itdb_cp_finalize() to obtain relevant update information for + * #Itdb_Track. + * + * Returns: a valid filename on the iPod where @filename can be + * copied or NULL in case of an error. In that case @error is set + * accordingly. You must free the filename when it is no longer + * needed. + * + * Since: 0.5.0 + */ +gchar *itdb_cp_get_dest_filename (Itdb_Track *track, + const gchar *mountpoint, + const gchar *filename, + GError **error) +{ + gchar *ipod_fullfile = NULL; + + /* either supply mountpoint or track */ + g_return_val_if_fail (mountpoint || track, NULL); + /* if mountpoint is not set, track->itdb is required */ + g_return_val_if_fail (mountpoint || track->itdb, NULL); + g_return_val_if_fail (filename, NULL); + + if (!mountpoint) + { + mountpoint = itdb_get_mountpoint (track->itdb); + } + + if (!mountpoint) + { + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Mountpoint not set.")); + return NULL; + } + + /* If track->ipod_path exists, we use that one instead. */ + if (track) + { + ipod_fullfile = itdb_filename_on_ipod (track); + } + + if (!ipod_fullfile) + { + gint dir_num, musicdirs_number; + gchar *dest_components[] = {NULL, NULL, NULL}; + gchar *parent_dir_filename, *music_dir; + gchar *original_suffix; + gchar dir_num_str[6]; + gint32 oops = 0; + gint32 rand = g_random_int_range (0, 899999); /* 0 to 900000 */ + + music_dir = itdb_get_music_dir (mountpoint); + if (!music_dir) + { + error_no_music_dir (mountpoint, error); + return NULL; + } + + if (track) + { + musicdirs_number = itdb_musicdirs_number (track->itdb); + } + else + { + musicdirs_number = itdb_musicdirs_number_by_mountpoint (mountpoint); + } + if (musicdirs_number <= 0) + { + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("No 'F..' directories found in '%s'."), + music_dir); + g_free (music_dir); + return NULL; + } + + dir_num = g_random_int_range (0, musicdirs_number); + + g_snprintf (dir_num_str, 6, "F%02d", dir_num); + dest_components[0] = dir_num_str; + + parent_dir_filename = + itdb_resolve_path (music_dir, (const gchar **)dest_components); + if(parent_dir_filename == NULL) + { + /* Can't find the F%02d directory */ + gchar *str = g_build_filename (music_dir, + dest_components[0], NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Path not found: '%s'."), + str); + g_free (str); + g_free (music_dir); + return NULL; + } + + /* 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 = ""; + + /* use lower-case version of extension as some iPods seem to + choke on upper-case extension. */ + original_suffix = g_ascii_strdown (original_suffix, -1); + + do + { /* we need to loop until we find an unused filename */ + dest_components[1] = + g_strdup_printf("libgpod%06d%s", + rand + oops, original_suffix); + ipod_fullfile = itdb_resolve_path ( + parent_dir_filename, + (const gchar **)&dest_components[1]); + 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[1], NULL); + } + g_free (dest_components[1]); + ++oops; + } while (!ipod_fullfile); + g_free(parent_dir_filename); + g_free (music_dir); + g_free (original_suffix); + } + + return ipod_fullfile; +} + +/** + * itdb_cp_finalize: + * @track: track to update or NULL + * @mountpoint: mountpoint of your iPod or NULL + * @dest_filename: the name of the file on the iPod copied to + * @error: return location for a #GError or NULL + * + * Updates information in @track necessary for the iPod. + * + * You must supply either @track or @mountpoint. If @track == NULL, a + * new track structure is created that must be freed with + * itdb_track_free() when it is no longer needed. + * + * The following @track fields are updated: + * + * + * + * ipod_path + * + * + * filetype_marker + * + * + * transferred + * + * + * size + * + * + * + * Returns: on success a pointer to the #Itdb_Track item passed + * or a new #Itdb_Track item if @track was NULL. In the latter case + * you must free the memory using itdb_track_free() when the item is + * no longer used. If an error occurs NULL is returned and @error is + * set accordingly. Errors occur when @dest_filename cannot be + * accessed or the mountpoint is not set. + * + * Since: 0.5.0 + */ +Itdb_Track *itdb_cp_finalize (Itdb_Track *track, + const gchar *mountpoint, + const gchar *dest_filename, + GError **error) +{ + const gchar *suffix; + Itdb_Track *use_track; + gint i, mplen; + struct stat statbuf; + + /* either supply mountpoint or track */ + g_return_val_if_fail (mountpoint || track, NULL); + /* if mountpoint is not set, track->itdb is required */ + g_return_val_if_fail (mountpoint || track->itdb, NULL); + g_return_val_if_fail (dest_filename, NULL); + + if (!mountpoint) + { + mountpoint = itdb_get_mountpoint (track->itdb); + } + + if (!mountpoint) + { + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Mountpoint not set.")); + return NULL; + } + + if (stat (dest_filename, &statbuf) == -1) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("'%s' could not be accessed (%s)."), + dest_filename, g_strerror (errno)); + return NULL; + } + + if (strlen (mountpoint) >= strlen (dest_filename)) + { + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Destination file '%s' does not appear to be on the iPod mounted at '%s'."), + dest_filename, mountpoint); + return NULL; + } + + if (!track) + { + use_track = itdb_track_new (); + } + else + { + use_track = track; + } + + use_track->transferred = TRUE; + use_track->size = statbuf.st_size; + + /* we need the original suffix of pcfile to construct a correct ipod + filename */ + suffix = strrchr (dest_filename, '.'); + /* If there is no ".mp3", ".m4a" etc, set original_suffix to empty + string. Note: the iPod will most certainly ignore this file... */ + if (!suffix) suffix = "."; + + /* set filetype from the suffix, e.g. '.mp3' -> 'MP3 ' */ + use_track->filetype_marker = 0; + for (i=1; i<=4; ++i) /* start with i=1 to skip the '.' */ + { + use_track->filetype_marker = use_track->filetype_marker << 8; + if (strlen (suffix) > i) + use_track->filetype_marker |= g_ascii_toupper (suffix[i]); + else + use_track->filetype_marker |= ' '; + } + + /* now extract filepath for use_track->ipod_path from ipod_fullfile */ + /* ipod_path must begin with a '/' */ + g_free (use_track->ipod_path); + mplen = strlen (mountpoint); /* length of mountpoint in bytes */ + if (dest_filename[mplen] == G_DIR_SEPARATOR) + { + use_track->ipod_path = g_strdup (&dest_filename[mplen]); + } + else + { + use_track->ipod_path = g_strdup_printf ("%c%s", G_DIR_SEPARATOR, + &dest_filename[mplen]); + } + /* convert to iPod type */ + itdb_filename_fs2ipod (use_track->ipod_path); + + return use_track; +} + +/** + * itdb_cp_track_to_ipod: + * @track: the #Itdb_Track to copy (containing @filename metadata) + * @filename: the source file + * @error: return location for a #GError or NULL + * + * Copy one track to the iPod. The PC filename is @filename + * and is taken literally. + * + * The mountpoint of the iPod (in local encoding) must have been set + * with itdb_set_mountpoint() (done automatically when reading an + * iTunesDB). + * + * If @track->transferred is set to TRUE, nothing is done. Upon + * successful transfer @track->transferred is set to TRUE. + * + * For storage, the directories "F00 ... Fnn" will be used randomly. + * + * The filename is constructed as "libgpod@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. + * + * @track->filetype_marker is set according to the filename extension + * + * Returns: TRUE on success, FALSE on error, in which case @error is + * set accordingly. + */ +gboolean itdb_cp_track_to_ipod (Itdb_Track *track, + const gchar *filename, GError **error) +{ + gchar *dest_filename; + gboolean result = FALSE; + + g_return_val_if_fail (track, FALSE); + g_return_val_if_fail (track->itdb, FALSE); + g_return_val_if_fail (itdb_get_mountpoint (track->itdb), FALSE); + g_return_val_if_fail (filename, FALSE); + + if(track->transferred) return TRUE; /* nothing to do */ + + dest_filename = itdb_cp_get_dest_filename (track, NULL, filename, error); + + if (dest_filename) + { + if (itdb_cp (filename, dest_filename, error)) + { + if (itdb_cp_finalize (track, NULL, dest_filename, error)) + { + result = TRUE; + } + } + g_free (dest_filename); + } + + return result; +} + +/** + * itdb_filename_on_ipod: + * @track: an #Itdb_Track + * + * Get the full iPod filename as stored in @track. + * + * + * NULL is returned when the file does not exist. + * + * + * + * 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 if + * you can avoid it. + * + * + * Returns: full filename to @track on the iPod or NULL if no + * filename is set in @track. Must be freed with g_free() after use. + */ +gchar *itdb_filename_on_ipod (Itdb_Track *track) +{ + gchar *result = NULL; + gchar *buf; + const gchar *mp; + + g_return_val_if_fail (track, NULL); + + if (!track->ipod_path || !*track->ipod_path) + { /* No filename set */ + return NULL; + } + + g_return_val_if_fail (track->itdb, NULL); + + if (!itdb_get_mountpoint (track->itdb)) return NULL; + + mp = itdb_get_mountpoint (track->itdb); + + 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; +} + +/* Use open instead of fopen. fwrite is really slow on the Mac. */ +/** + * itdb_cp: + * @from_file: source file + * @to_file: destination file + * @error: return location for a #GError or NULL + * + * Copy file @from_file to @to_file. + * + * Returns: TRUE on success, FALSE on error, in which case @error is + * set accordingly. + */ +gboolean itdb_cp (const gchar *from_file, const gchar *to_file, + GError **error) +{ +#ifndef O_BINARY +#define O_BINARY 0 +#endif + gchar *data; + glong bread, bwrite; + int file_in = -1; + int file_out = -1; + +#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 = g_open (from_file, O_RDONLY | O_BINARY, 0); + if (file_in < 0) + { + 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 = g_open (to_file, O_CREAT|O_WRONLY|O_TRUNC|O_BINARY, 0777); + if (file_out < 0) + { + 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 = read (file_in, data, ITUNESDB_COPYBLK); +#if ITUNESDB_DEBUG + fprintf(stderr, "itunesdb_cp: read %ld bytes\n", bread); +#endif + if (bread < 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 = write (file_out, data, bread); +#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 (close (file_in) != 0) + { + file_in = -1; + 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 (close (file_out) != 0) + { + file_out = -1; + 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 >= 0) close (file_in); + if (file_out >= 0) close (file_out); + g_unlink (to_file); + g_free (data); + return FALSE; +} + +/** + * itdb_get_control_dir: + * @mountpoint: the iPod mountpoint + * + * Get the i*_Control directory. Observed values are 'iPod_Control' + * for standard iPods and 'iTunes/iTunes_Control' for mobile + * applications. + * + * Returns: path to the control dir or NULL if non-existent. Must + * g_free() after use. + * + * Since: 0.4.0 + */ +gchar *itdb_get_control_dir (const gchar *mountpoint) +{ + gchar *p_iphone[] = {"iTunes_Control", NULL}; + gchar *p_ipod[] = {"iPod_Control", NULL}; + gchar *p_mobile[] = {"iTunes", "iTunes_Control", NULL}; + /* Use an array with all possibilities, so further extension will + be easy. It is important that checking for the iTunes_Control + directory be done before the iPod_Control directory because + some devices actually have both directories. The iTunes_Control + directory is correct in these cases. This happens for devices + that Apple shipped with the iPod_Control as the control directory + and later switched to iTunes_Control instead. iTunes appears to + remove files from the old iPod_Control directory but leave the + directory structure intact. Finding this empty directory structure + first will result in a failure to find files. */ + gchar **paths[] = {p_iphone, p_ipod, p_mobile, NULL}; + gchar ***ptr; + gchar *result = NULL; + + g_return_val_if_fail (mountpoint, NULL); + + for (ptr=paths; *ptr && !result; ++ptr) + { + result = itdb_resolve_path (mountpoint, (const gchar **)*ptr); + } + return result; +} + +/** + * itdb_get_dir: + * @mountpoint: the iPod mountpoint + * @dir: a directory + * + * Retrieve the directory @dir by first calling itdb_get_control_dir() + * and then adding @dir + * + * Returns: path to @dir or NULL if non-existent. Must g_free() + * after use. + */ +static gchar *itdb_get_dir (const gchar *mountpoint, const gchar *dir) +{ + gchar *control_dir; + gchar *result = NULL; + + g_return_val_if_fail (mountpoint, NULL); + g_return_val_if_fail (dir, NULL); + + control_dir = itdb_get_control_dir (mountpoint); + if (control_dir) + { + const gchar *p_dir[] = {NULL, NULL}; + p_dir[0] = dir; + result = itdb_resolve_path (control_dir, p_dir); + g_free (control_dir); + } + return result; +} + +/** + * itdb_get_path: + * @dir: a directory + * @file: a file + * + * Retrieve a path to the @file in @dir + * + * Returns: path to the @file or NULL if non-existent. Must g_free() + * after use. + * + * Since: 0.4.0 + */ +gchar *itdb_get_path (const gchar *dir, const gchar *file) +{ + const gchar *p_file[] = {NULL, NULL}; + + g_return_val_if_fail (dir, NULL); + + p_file[0] = file; + + return itdb_resolve_path (dir, p_file); +} + +/** + * itdb_get_itunes_dir: + * @mountpoint: the iPod mountpoint + * + * Retrieve the iTunes directory (containing the iTunesDB) by first + * calling itdb_get_control_dir() and then adding 'iTunes' + * + * Returns: path to the iTunes directory or NULL if non-existent. + * Must g_free() after use. + * + * Since: 0.4.0 + */ +gchar *itdb_get_itunes_dir (const gchar *mountpoint) +{ + g_return_val_if_fail (mountpoint, NULL); + + return itdb_get_dir (mountpoint, "iTunes"); +} + +/** + * itdb_get_music_dir: + * @mountpoint: the iPod mountpoint + * + * Retrieve the Music directory (containing the Fnn dirs) by first + * calling itdb_get_control_dir() and then adding 'Music' + * + * Returns: path to the Music directory or NULL if + * non-existent. Must g_free() after use. + */ +gchar *itdb_get_music_dir (const gchar *mountpoint) +{ + g_return_val_if_fail (mountpoint, NULL); + + return itdb_get_dir (mountpoint, "Music"); +} + +/** + * itdb_get_device_dir: + * @mountpoint: the iPod mountpoint + * + * Retrieve the Device directory (containing the SysInfo file) by + * first calling itdb_get_control_dir() and then adding 'Device' + * + * Returns: path to the Device directory or NULL if + * non-existent. Must g_free() after use. + * + * Since: 0.4.0 + */ +gchar *itdb_get_device_dir (const gchar *mountpoint) +{ + g_return_val_if_fail (mountpoint, NULL); + + return itdb_get_dir (mountpoint, "Device"); +} + +/** + * itdb_get_artwork_dir: + * @mountpoint: the iPod mountpoint + * + * Retrieve the Artwork directory (containing the ArtworDB) by + * first calling itdb_get_control_dir() and then adding 'Artwork' + * + * Returns: path to the Artwork directory or NULL if + * non-existent. Must g_free() after use. + * + * Since: 0.4.0 + */ +gchar *itdb_get_artwork_dir (const gchar *mountpoint) +{ + g_return_val_if_fail (mountpoint, NULL); + + return itdb_get_dir (mountpoint, "Artwork"); +} + +/** + * itdb_get_itunesdb_path: + * @mountpoint: the iPod mountpoint + * + * Retrieve a path to the iTunesDB + * + * Returns: path to the iTunesDB or NULL if non-existent. Must g_free() + * after use. + * + * Since: 0.4.0 + */ +gchar *itdb_get_itunesdb_path (const gchar *mountpoint) +{ + gchar *itunes_dir, *path=NULL; + + g_return_val_if_fail (mountpoint, NULL); + + itunes_dir = itdb_get_itunes_dir (mountpoint); + + if (itunes_dir) + { + path = itdb_get_path (itunes_dir, "iTunesCDB"); + if(!path) { + path = itdb_get_path (itunes_dir, "iTunesDB"); + } + g_free (itunes_dir); + } + + return path; +} + +/** + * itdb_get_itunescdb_path: + * @mountpoint: the iPod mountpoint + * + * Retrieve a path to the iTunesCDB. The iTunesCDB is a compressed version + * of the iTunesDB which can be found on iPhones/iPod Touch with firmware 3.0 + * + * + * Returns: path to the iTunesCDB or NULL if non-existent. Must g_free() + * after use. + * + * Since: 0.4.0 + */ +gchar *itdb_get_itunescdb_path (const gchar *mountpoint) +{ + gchar *itunes_dir, *path=NULL; + + g_return_val_if_fail (mountpoint, NULL); + + itunes_dir = itdb_get_itunes_dir (mountpoint); + + if (itunes_dir) + { + path = itdb_get_path (itunes_dir, "iTunesCDB"); + g_free (itunes_dir); + } + + return path; +} + +/** + * itdb_get_itunessd_path: + * @mountpoint: the iPod mountpoint + * + * Retrieve a path to the iTunesSD + * + * Returns: path to the iTunesSD or NULL if non-existent. Must g_free() + * after use. + * + * Since: 0.4.0 + */ +gchar *itdb_get_itunessd_path (const gchar *mountpoint) +{ + gchar *itunes_dir, *path=NULL; + + g_return_val_if_fail (mountpoint, NULL); + + itunes_dir = itdb_get_itunes_dir (mountpoint); + + if (itunes_dir) + { + path = itdb_get_path (itunes_dir, "iTunesSD"); + g_free (itunes_dir); + } + + return path; +} + +/** + * itdb_get_artworkdb_path: + * @mountpoint: the iPod mountpoint + * + * Retrieve a path to the ArtworkDB + * + * Returns: path to the ArtworkDB or NULL if non-existent. Must g_free() + * after use. + * + * Since: 0.4.0 + */ +gchar *itdb_get_artworkdb_path (const gchar *mountpoint) +{ + gchar *itunes_dir, *path=NULL; + + g_return_val_if_fail (mountpoint, NULL); + + itunes_dir = itdb_get_artwork_dir (mountpoint); + + if (itunes_dir) + { + path = itdb_get_path (itunes_dir, "ArtworkDB"); + g_free (itunes_dir); + } + + return path; +} + + +/*------------------------------------------------------------------*\ + * * + * Timestamp stuff * + * * +\*------------------------------------------------------------------*/ + +/** + * itdb_time_get_mac_time: + * + * Gets the current time in a format appropriate for storing in the libgpod + * data structures + * + * Returns: current time + * + * Deprecated: kept for compatibility with older code, directly use + * g_get_current_time() or time(NULL) instead + */ +time_t itdb_time_get_mac_time (void) +{ + GTimeVal time; + + g_get_current_time (&time); + + return time.tv_sec; +} + +/** + * itdb_time_mac_to_host: + * @time: time expressed in libgpod format + * + * Converts a timestamp from libgpod format to host system timestamp. + * + * Returns: timestamp for the host system + * + * Deprecated: It's been kept for compatibility with older code, but this + * function is now a no-op + */ +time_t itdb_time_mac_to_host (time_t time) +{ + return time; +} + +/** + * itdb_time_host_to_mac: + * @time: time expressed in host unit + * + * Convert host system timestamp to libgpod format timestamp + * + * Returns: a libgpod timestamp + * + * Deprecated: It's been kept for compatibility with older code, but this + * function is now a no-op + */ +time_t itdb_time_host_to_mac (time_t time) +{ + return time; +} + +/** + * itdb_init_ipod: + * @mountpoint: the iPod mountpoint + * @model_number: the iPod model number, can be NULL + * @ipod_name: the name to give to the iPod. Will be displayed in + * gtkpod or itunes + * @error: return location for a #GError or NULL + * + * Initialise an iPod device from scratch. The function attempts to + * create a blank database, complete with master playlist and device + * information as well as the directory structure required for the + * type of iPod. + * @model_number is used to tell libgpod about the exact iPod + * model, which is needed for proper artwork writing. @model_number can be + * found from the table returned by itdb_device_get_ipod_info_table (for + * example). On recent distros with iPods released + * in the last few years (starting with the iPod Color), it should be fine + * to pass in a NULL @model_number while still getting artwork writing. + * + * Returns: TRUE when successful, FALSE if a failure has occurred. + * + * Since: 0.4.0 + */ +gboolean itdb_init_ipod (const gchar *mountpoint, + const gchar *model_number, + const gchar *ipod_name, + GError **error) +{ + gboolean writeok; + Itdb_iTunesDB *itdb = NULL; + Itdb_Playlist *mpl = NULL; + gchar *path; + + g_return_val_if_fail (mountpoint, FALSE); + + /* Create new blank itdb database for writing to iPod */ + itdb = itdb_new(); + + /* Assign iPod device reference to new database */ + itdb_set_mountpoint(itdb, mountpoint); + + /* Insert model_number into sysinfo file if present + * The model number can be extracted in a couple of ways: + * - use the read_sysinfo_file function + * - use libipoddevice and hal to get the model + * (as far as I know, libipoddevice will also + * read the sysinfo file, complemented by some + * guessing). + */ + if (model_number) + { + itdb_device_set_sysinfo (itdb->device, + "ModelNumStr", model_number); + } + + /* Create the remaining directories resident on blank ipod */ + writeok = itdb_create_directories(itdb->device, error); + if(! writeok) + { + return FALSE; + } + + /* Create a new playlist with the desired name of the ipod + * and set it as the mpl */ + if (ipod_name == NULL) + { + mpl = itdb_playlist_new(_("iPod"), FALSE); + } + else + { + mpl = itdb_playlist_new(ipod_name, FALSE); + } + itdb_playlist_set_mpl(mpl); + itdb_playlist_add(itdb, mpl, -1); + + /* Write both the iTunesDB and iTunesSD files to the new ipod, + * unless they already exist */ + path = itdb_get_itunesdb_path (mountpoint); + if (!path) + { + writeok = itdb_write(itdb, error); + if(! writeok) + { + itdb_free (itdb); + return FALSE; + } + } + g_free (path); + + /* If model is a shuffle or the model is undetermined, + * ie. @model_number is NULL, then create the itunesSD database + */ + if(!model_number || itdb_device_is_shuffle (itdb->device)) + { + path = itdb_get_itunessd_path (mountpoint); + if (!path) + { + writeok = itdb_shuffle_write(itdb, error); + if(! writeok) + { + itdb_free (itdb); + return FALSE; + } + } + g_free (path); + } + itdb_free (itdb); + return TRUE; +} + +/** + * itdb_chapterdata_build_chapter_blob: + * @chapterdata: Itdb_Chapterdata pointer of chapter data to be encoded + * + * Creates an iTunesDB binary blob of chapter data from @chapterdata. + * This helper function is used by both mk_mhod() in itdb_itunesdb.c + * and mk_Extras() in itdb_sqlite.c, so take care when updating to + * maintain compatibility with both chapterdata blobs. + * + * NOTE: Caller must call g_byte_array_free(chapter_blob, TRUE) on the + * returned chapter_blob + * + * Returns: a binary itdb blob of chapter data (to be freed by the caller) + * + * Since: 0.7.95 + */ +G_GNUC_INTERNAL GByteArray *itdb_chapterdata_build_chapter_blob(Itdb_Chapterdata *chapterdata, gboolean reversed) +{ + WContents *cts; + GByteArray * chapter_blob = NULL; + + cts = wcontents_new(""); + cts->reversed = reversed; + cts->pos = 0; + + itdb_chapterdata_build_chapter_blob_internal (cts, chapterdata); + + chapter_blob = g_byte_array_sized_new(cts->pos); + g_byte_array_append(chapter_blob, (guint8 *)cts->contents, cts->pos); + wcontents_free(cts); + return chapter_blob; +} + + +/*------------------------------------------------------------------*\ + * * + * Some functions to access Itdb_DB safely * + * * +\*------------------------------------------------------------------*/ +G_GNUC_INTERNAL +Itdb_iTunesDB *db_get_itunesdb (Itdb_DB *db) +{ + g_return_val_if_fail (db, NULL); + g_return_val_if_fail (db->db_type == DB_TYPE_ITUNES, NULL); + + return db->db.itdb; +} + +G_GNUC_INTERNAL +Itdb_PhotoDB *db_get_photodb (Itdb_DB *db) +{ + g_return_val_if_fail (db, NULL); + g_return_val_if_fail (db->db_type == DB_TYPE_PHOTO, NULL); + + return db->db.photodb; +} + +G_GNUC_INTERNAL +Itdb_Device *db_get_device(Itdb_DB *db) +{ + g_return_val_if_fail (db, NULL); + + switch (db->db_type) { + case DB_TYPE_ITUNES: + g_return_val_if_fail (db_get_itunesdb(db), NULL); + return db_get_itunesdb(db)->device; + case DB_TYPE_PHOTO: + g_return_val_if_fail (db_get_photodb(db), NULL); + return db_get_photodb(db)->device; + } + g_return_val_if_reached (NULL); +} + +G_GNUC_INTERNAL +gchar *db_get_mountpoint(Itdb_DB *db) +{ + Itdb_Device *device; + g_return_val_if_fail (db, NULL); + + device = db_get_device (db); + g_return_val_if_fail (device, NULL); + + return device->mountpoint; +} + +static gchar *itdb_device_get_control_dir (const Itdb_Device *device) +{ + Itdb_IpodInfo const *info = NULL; + char *podpath; + + podpath = itdb_get_control_dir (device->mountpoint); + if (podpath != NULL) { + return podpath; + } + + /* The device doesn't already have an iPod_Control directory, let's try + * to get which one is appropriate to use + */ + info = itdb_device_get_ipod_info (device); + + if (itdb_device_is_shuffle (device)) { + podpath = g_build_filename (device->mountpoint, "iPod_Control", NULL); + } else if (itdb_device_is_iphone_family (device)) { + podpath = g_build_filename (device->mountpoint,"iTunes_Control", NULL); + } else if (info->ipod_model == ITDB_IPOD_MODEL_MOBILE_1) { + podpath = g_build_filename (device->mountpoint, + "iTunes", "iTunes_Control", + NULL); + } else { + podpath = g_build_filename (device->mountpoint, "iPod_Control", NULL); + } + + return podpath; +} + + +/*------------------------------------------------------------------*\ + * * + * Create iPod directory hierarchy * + * * +\*------------------------------------------------------------------*/ +static gboolean itdb_create_directories (Itdb_Device *device, GError **error) +{ + const gchar *mp; + gboolean result; + gchar *pbuf; + gint i, dirnum; + gchar *podpath = NULL; + gchar *model_number; + Itdb_IpodInfo const *info = NULL; + + g_return_val_if_fail (device, FALSE); + + mp = device->mountpoint; + info = itdb_device_get_ipod_info (device); + + g_return_val_if_fail (mp, FALSE); + + /* Construct the Control directory */ + pbuf = itdb_device_get_control_dir (device); + if (!g_file_test (pbuf, G_FILE_TEST_EXISTS)) + { + if (g_mkdir_with_parents(pbuf, 0777) != 0) + { + goto error_dir; + } + } + g_free (pbuf); + podpath = itdb_get_control_dir (mp); + if (!podpath) { + goto error_dir; + } + + /* Construct the Music directory inside the Control directory */ + pbuf = g_build_filename (podpath, "Music", NULL); + if (!g_file_test (pbuf, G_FILE_TEST_EXISTS)) + { + if((g_mkdir(pbuf, 0777) != 0)) + { + goto error_dir; + } + } + g_free (pbuf); + + /* Construct the iTunes directory inside the Control directory */ + pbuf = g_build_filename (podpath, "iTunes", NULL); + if (!g_file_test (pbuf, G_FILE_TEST_EXISTS)) + { + if((g_mkdir(pbuf, 0777) != 0)) + { + goto error_dir; + } + } + g_free (pbuf); + + /* Build Artwork directory only for devices requiring artwork + * (assume that 'unknown models' are new and will support + * artwork) */ + if (itdb_device_supports_artwork(device) || + (info->ipod_model == ITDB_IPOD_MODEL_UNKNOWN)) + { + pbuf = g_build_filename (podpath, "Artwork", NULL); + if (!g_file_test (pbuf, G_FILE_TEST_EXISTS)) + { + if((g_mkdir(pbuf, 0777) != 0)) { + goto error_dir; + } + } + g_free (pbuf); + } + + /* Build Photo directory only for devices requiring it */ + if (itdb_device_supports_photo(device) || + (info->ipod_model == ITDB_IPOD_MODEL_UNKNOWN)) + { + pbuf = g_build_filename (mp, "Photos", "Thumbs", NULL); + if (!g_file_test (pbuf, G_FILE_TEST_EXISTS)) + { + if (g_mkdir_with_parents(pbuf, 0777) != 0) + { + goto error_dir; + } + } + g_free (pbuf); + } + + /* Build the directories that hold the music files */ + dirnum = info->musicdirs; + if (dirnum == 0) + { + guint64 capacity, free_space; + if (itdb_device_get_storage_info(device, &capacity, &free_space)) { + gdouble size = ((gdouble)capacity) / 1073741824; + if (size < 20) dirnum = 20; + else dirnum = 50; + } else { + dirnum = 20; + } + } + + for(i = 0; i < dirnum; i++) + { + gchar *num = g_strdup_printf ("F%02d", i); + pbuf = g_build_filename (podpath, "Music", num, NULL); + g_free (num); + if (!g_file_test (pbuf, G_FILE_TEST_EXISTS)) + { + if((g_mkdir(pbuf, 0777) != 0)) + { + goto error_dir; + } + } + g_free (pbuf); + } + + /* Build Calendar directory for models requiring it */ + if (!itdb_device_is_iphone_family (device) + && !itdb_device_is_shuffle (device)) + { + pbuf = g_build_filename (mp, "Calendars", NULL); + if (!g_file_test (pbuf, G_FILE_TEST_EXISTS)) + { + if((g_mkdir(pbuf, 0777) != 0)) + { + goto error_dir; + } + } + g_free (pbuf); + + /* Build Contacts directory for models requiring it */ + pbuf = g_build_filename (mp, "Contacts", NULL); + if (!g_file_test (pbuf, G_FILE_TEST_EXISTS)) + { + if((g_mkdir(pbuf, 0777) != 0)) + { + goto error_dir; + } + } + g_free (pbuf); + + pbuf = g_build_filename (mp, "Notes", NULL); + if (!g_file_test (pbuf, G_FILE_TEST_EXISTS)) + { + if((g_mkdir(pbuf, 0777) != 0)) + { + goto error_dir; + } + } + g_free (pbuf); + } + + /* Construct a Device directory file for special models */ + pbuf = g_build_filename (podpath, "Device", NULL); + if (!g_file_test (pbuf, G_FILE_TEST_EXISTS)) + { + if((g_mkdir(pbuf, 0777) != 0)) + { + goto error_dir; + } + } + g_free (pbuf); + + model_number = itdb_device_get_sysinfo (device, "ModelNumStr"); + /* Construct a SysInfo file */ + if (model_number && (strlen (model_number) != 0)) + { + pbuf = NULL; + if (!itdb_device_write_sysinfo (device, error)) + { + g_free (model_number); + goto error_dir; + } + } + g_free (model_number); + pbuf = NULL; + + error_dir: + if (pbuf) + { + g_set_error (error, 0, -1, + _("Problem creating iPod directory or file: '%s'."), + pbuf); + result = FALSE; + } else + { + result = TRUE; + } + g_free (pbuf); + g_free (podpath); + return result; +}