""" iPod iTunesDB parser. Documentation: - http://ipl.derpapst.org/wiki/ITunesDB/iTunesDB_File Author: Romain HERAULT Creation date: 19 august 2006 """ from hachoir.parser import Parser from hachoir.field import (FieldSet, UInt8, UInt16, UInt32, Int32, UInt64, TimestampMac32, String, Float32, NullBytes, Enum, RawBytes) from hachoir.core.endian import LITTLE_ENDIAN from hachoir.core.tools import humanDuration from hachoir.core.text_handler import displayHandler, filesizeHandler list_order = { 1: "playlist order (manual sort order)", 2: "???", 3: "songtitle", 4: "album", 5: "artist", 6: "bitrate", 7: "genre", 8: "kind", 9: "date modified", 10: "track number", 11: "size", 12: "time", 13: "year", 14: "sample rate", 15: "comment", 16: "date added", 17: "equalizer", 18: "composer", 19: "???", 20: "play count", 21: "last played", 22: "disc number", 23: "my rating", 24: "release date", 25: "BPM", 26: "grouping", 27: "category", 28: "description", 29: "show", 30: "season", 31: "episode number" } class DataObject(FieldSet): type_name = { 1: "Title", 2: "Location", 3: "Album", 4: "Artist", 5: "Genre", 6: "Filetype", 7: "EQ Setting", 8: "Comment", 9: "Category", 12: "Composer", 13: "Grouping", 14: "Description text", 15: "Podcast Enclosure URL", 16: "Podcast RSS URL", 17: "Chapter data", 18: "Subtitle", 19: "Show (for TV Shows only)", 20: "Episode", 21: "TV Network", 22: "Album-Artist", 23: "Artist for Sorting", 24: "List of keywords pretaining track", 25: "Locale for TV show(?)", 27: "Title for Sorting", 28: "Album for Sorting", 29: "Album-Artist for Sorting", 30: "Composer for Sorting", 31: "Show for Sorting", # 32:"Unknown binary field for video tracks", 50: "Smart Playlist Data", 51: "Smart Playlist Rules", 52: "Library Playlist Index", 53: "Library Playlist Index letter in jump table", 100: "Ccolumn Sizing Info as well as an order indicator in playlists.", 102: "For iPhone", 200: "Album name (for album descriptions)", 201: "Album artist (for album descriptions)", 202: "Album sort artist (for album descriptions)", 203: "Podcast URL in Album List", 204: "TV Show in Album List" } mhod52_sort_index_type_name = { 3: "Title", 4: "Album, then Disk/Tracknumber, then Title", 5: "Artist, then Album, then Disc/Tracknumber, then Title", 7: "Genre, then Artist, then Album, then Disc/Tracknumber, then Title", 8: "Composer, then Title" } def __init__(self, *args, **kw): FieldSet.__init__(self, *args, **kw) self._size = self["entry_length"].value * 8 def createFields(self): yield String(self, "header_id", 4, "Data Object Header Markup (\"mhod\")", charset="ISO-8859-1") yield UInt32(self, "header_length", "Header Length") yield UInt32(self, "entry_length", "Entry Length") yield Enum(UInt32(self, "type", "type"), self.type_name) if (self["type"].value == 15) or (self["type"].value == 16): yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield String(self, "string", self._size // 8 - self["header_length"].value, "String Data", charset="UTF-8") elif (self["type"].value == 52): yield UInt32(self, "unknown[]", "unk1") yield UInt32(self, "unknown[]", "unk2") yield Enum(UInt32(self, "sort_index_type", "Sort Index Type"), self.mhod52_sort_index_type_name) yield UInt32(self, "entry_count", "Entry Count") indexes_size = self["entry_count"].value * 4 padding_offset = self["entry_length"].value - indexes_size padding = self.seekByte(padding_offset, "header padding") if padding: yield padding for i in xrange(self["entry_count"].value): yield UInt32(self, "index[" + str(i) + "]", "Index of the " + str(i) + "nth mhit") elif (self["type"].value < 15) or (self["type"].value > 17) or (self["type"].value >= 200): yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt32(self, "position", "Position") yield UInt32(self, "length", "String Length in bytes") yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield String(self, "string", self["length"].value, "String Data", charset="UTF-16-LE") else: padding = self.seekByte(self["header_length"].value, "header padding") if padding: yield padding padding = self.seekBit(self._size, "entry padding") if padding: yield padding class TrackItem(FieldSet): x1_type_name = { 0: "AAC or CBR MP3", 1: "VBR MP3" } x2_type_name = { 0: "AAC", 1: "MP3" } media_type_name = { 0x00: "Audio/Video", 0x01: "Audio", 0x02: "Video", 0x04: "Podcast", 0x06: "Video Podcast", 0x08: "Audiobook", 0x20: "Music Video", 0x40: "TV Show", 0X60: "TV Show (Music lists)", } def __init__(self, *args, **kw): FieldSet.__init__(self, *args, **kw) self._size = self["entry_length"].value * 8 def createFields(self): yield String(self, "header_id", 4, "Track Item Header Markup (\"mhit\")", charset="ISO-8859-1") yield UInt32(self, "header_length", "Header Length") yield UInt32(self, "entry_length", "Entry Length") yield UInt32(self, "string_number", "Number of Strings") yield UInt32(self, "unique_id", "Unique ID") yield UInt32(self, "visible_tag", "Visible Tag") yield String(self, "file_type", 4, "File Type") yield Enum(UInt8(self, "x1_type", "Extended Type 1"), self.x1_type_name) yield Enum(UInt8(self, "x2_type", "Extended type 2"), self.x2_type_name) yield UInt8(self, "compilation_flag", "Compilation Flag") yield UInt8(self, "rating", "Rating") yield TimestampMac32(self, "last_modified", "Time of the last modification of the track") yield filesizeHandler(UInt32(self, "size", "Track size in bytes")) yield displayHandler(UInt32(self, "length", "Track length in milliseconds"), humanDuration) yield UInt32(self, "track_number", "Number of this track") yield UInt32(self, "total_track", "Total number of tracks") yield UInt32(self, "year", "Year of the track") yield UInt32(self, "bitrate", "Bitrate") yield UInt32(self, "samplerate", "Sample Rate") yield UInt32(self, "volume", "volume") yield UInt32(self, "start_time", "Start playing at, in milliseconds") yield UInt32(self, "stop_time", "Stop playing at, in milliseconds") yield UInt32(self, "soundcheck", "SoundCheck preamp") yield UInt32(self, "playcount_1", "Play count of the track") yield UInt32(self, "playcount_2", "Play count of the track when last synced") yield TimestampMac32(self, "last_played_time", "Time the song was last played") yield UInt32(self, "disc_number", "disc number in multi disc sets") yield UInt32(self, "total_discs", "Total number of discs in the disc set") yield UInt32(self, "userid", "User ID in the DRM scheme") yield TimestampMac32(self, "added_date", "Date when the item was added") yield UInt32(self, "bookmark_time", "Bookmark time for AudioBook") yield UInt64(self, "dbid", "Unique DataBase ID for the song (identical in mhit and in mhii)") yield UInt8(self, "checked", "song is checked") yield UInt8(self, "application_rating", "Last Rating before change") yield UInt16(self, "BPM", "BPM of the track") yield UInt16(self, "artwork_count", "number of artworks for this item") yield UInt16(self, "unknown[]") yield UInt32(self, "artwork_size", "Total size of artworks in bytes") yield UInt32(self, "unknown[]") yield Float32(self, "sample_rate_2", "Sample Rate express in float") yield UInt32(self, "released_date", "Date of release in Music Store or in Podcast") yield UInt16(self, "unknown[]") yield UInt16(self, "explicit_flag[]", "Explicit flag") yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt32(self, "skip_count[]", "Skip Count") yield TimestampMac32(self, "last_skipped", "Date when the item was last skipped") yield UInt8(self, "has_artwork", "0x01 for track with artwork, 0x02 otherwise") yield UInt8(self, "skip_wen_shuffling", "Skip that track when shuffling") yield UInt8(self, "remember_playback_position", "Remember playback position") yield UInt8(self, "flag4", "Flag 4") yield UInt64(self, "dbid2", "Unique DataBase ID for the song (identical as above)") yield UInt8(self, "lyrics_flag", "Lyrics Flag") yield UInt8(self, "movie_file_flag", "Movie File Flag") yield UInt8(self, "played_mark", "Track has been played") yield UInt8(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt32(self, "pregap[]", "Number of samples of silence before the song starts") yield UInt64(self, "sample_count", "Number of samples in the song (only for WAV and AAC files)") yield UInt32(self, "unknown[]") yield UInt32(self, "postgap[]", "Number of samples of silence at the end of the song") yield UInt32(self, "unknown[]") yield Enum(UInt32(self, "media_type", "Media Type for video iPod"), self.media_type_name) yield UInt32(self, "season_number", "Season Number") yield UInt32(self, "episode_number", "Episode Number") yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt32(self, "gapless_data[]", "The size in bytes from first Sync Frame until the 8th before the last frame.") yield UInt32(self, "unknown[]") yield UInt16(self, "gaplessTrackFlag[]", "1 if track has gapless data") yield UInt16(self, "gaplessAlbumFlag[]", "1 if track uses crossfading in iTunes") yield RawBytes(self, "unknown[]", 20) yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt16(self, "unknown[]") yield UInt16(self, "album_id[]", "Album ID (used to link tracks with MHIAs)") yield RawBytes(self, "unknown[]", 52) yield UInt32(self, "mhii_link[]", "Artwork ID (used to link tracks with MHIIs)") padding = self.seekByte(self["header_length"].value, "header padding") if padding: yield padding # while ((self.stream.readBytes(0, 4) == 'mhod') and ((self.current_size/8) < self["entry_length"].value)): for i in xrange(self["string_number"].value): yield DataObject(self, "data[]") padding = self.seekBit(self._size, "entry padding") if padding: yield padding class TrackList(FieldSet): def createFields(self): yield String(self, "header_id", 4, "Track List Header Markup (\"mhlt\")", charset="ISO-8859-1") yield UInt32(self, "header_length", "Header Length") yield UInt32(self, "track_number", "Number of Tracks") padding = self.seekByte(self["header_length"].value, "header padding") if padding: yield padding for i in xrange(self["track_number"].value): yield TrackItem(self, "track[]") class PlaylistItem(FieldSet): def __init__(self, *args, **kw): FieldSet.__init__(self, *args, **kw) self._size = self["entry_length"].value * 8 def createFields(self): yield String(self, "header_id", 4, "Playlist Item Header Markup (\"mhip\")", charset="ISO-8859-1") yield UInt32(self, "header_length", "Header Length") yield UInt32(self, "entry_length", "Entry Length") yield UInt32(self, "data_object_child_count", "Number of Child Data Objects") yield UInt32(self, "podcast_grouping_flag", "Podcast Grouping Flag") yield UInt32(self, "group_id", "Group ID") yield UInt32(self, "track_id", "Track ID") yield TimestampMac32(self, "timestamp", "Song Timestamp") yield UInt32(self, "podcast_grouping_ref", "Podcast Grouping Reference") padding = self.seekByte(self["header_length"].value, "header padding") if padding: yield padding for i in xrange(self["data_object_child_count"].value): yield DataObject(self, "mhod[]") class Playlist(FieldSet): is_master_pl_name = { 0: "Regular playlist", 1: "Master playlist" } is_podcast_name = { 0: "Normal Playlist List", 1: "Podcast Playlist List" } list_sort_order_name = { 1: "Manual Sort Order", 2: "???", 3: "Song Title", 4: "Album", 5: "Artist", 6: "Bitrate", 7: "Genre", 8: "Kind", 9: "Date Modified", 10: "Track Number", 11: "Size", 12: "Time", 13: "Year", 14: "Sample Rate", 15: "Comment", 16: "Date Added", 17: "Equalizer", 18: "Composer", 19: "???", 20: "Play Count", 21: "Last Played", 22: "Disc Number", 23: "My Rating", 24: "Release Date", 25: "BPM", 26: "Grouping", 27: "Category", 28: "Description", 29: "Show", 30: "Season", 31: "Episode Number" } def __init__(self, *args, **kw): FieldSet.__init__(self, *args, **kw) self._size = self["entry_length"].value * 8 def createFields(self): yield String(self, "header_id", 4, "Playlist Header Markup (\"mhyp\")", charset="ISO-8859-1") yield UInt32(self, "header_length", "Header Length") yield UInt32(self, "entry_length", "Entry Length") yield UInt32(self, "data_object_child_count", "Number of Child Data Objects") yield UInt32(self, "playlist_count", "Number of Playlist Items") yield Enum(UInt8(self, "type", "Normal or master playlist?"), self.is_master_pl_name) yield UInt8(self, "XXX1", "XXX1") yield UInt8(self, "XXX2", "XXX2") yield UInt8(self, "XXX3", "XXX3") yield TimestampMac32(self, "creation_date", "Date when the playlist was created") yield UInt64(self, "playlistid", "Persistent Playlist ID") yield UInt32(self, "unk3", "unk3") yield UInt16(self, "string_mhod_count", "Number of string MHODs for this playlist") yield Enum(UInt16(self, "is_podcast", "Playlist or Podcast List?"), self.is_podcast_name) yield Enum(UInt32(self, "sort_order", "Playlist Sort Order"), self.list_sort_order_name) padding = self.seekByte(self["header_length"].value, "entry padding") if padding: yield padding for i in xrange(self["data_object_child_count"].value): yield DataObject(self, "mhod[]") for i in xrange(self["playlist_count"].value): yield PlaylistItem(self, "playlist_item[]") class PlaylistList(FieldSet): def createFields(self): yield String(self, "header_id", 4, "Playlist List Header Markup (\"mhlp\")", charset="ISO-8859-1") yield UInt32(self, "header_length", "Header Length") yield UInt32(self, "playlist_number", "Number of Playlists") padding = self.seekByte(self["header_length"].value, "header padding") if padding: yield padding for i in xrange(self["playlist_number"].value): yield Playlist(self, "playlist[]") class Album(FieldSet): def __init__(self, *args, **kw): FieldSet.__init__(self, *args, **kw) self._size = self["entry_length"].value * 8 def createFields(self): yield String(self, "header_id", 4, "Album Item Header Markup (\"mhia\")", charset="ISO-8859-1") yield UInt32(self, "header_length", "Header Length") yield UInt32(self, "entry_length", "Entry Length") yield UInt32(self, "data_object_child_count", "Number of Child Data Objects") yield UInt16(self, "unknow[]") yield UInt16(self, "album_id[]", "Album ID") yield UInt32(self, "unknow[]") yield UInt32(self, "unknow[]") yield UInt32(self, "unknow[]") padding = self.seekByte(self["header_length"].value, "entry padding") if padding: yield padding for i in xrange(self["data_object_child_count"].value): yield DataObject(self, "mhod[]") class AlbumList(FieldSet): def createFields(self): yield String(self, "header_id", 4, "Album List Header Markup (\"mhla\")", charset="ISO-8859-1") yield UInt32(self, "header_length", "Header Length") yield UInt32(self, "album_number", "Number of Albums") padding = self.seekByte(self["header_length"].value, "header padding") if padding: yield padding for i in xrange(self["album_number"].value): yield Album(self, "album[]") class DataSet(FieldSet): type_name = { 1: "Track List", 2: "Play List", 3: "Podcast List", 4: "Album List" } def __init__(self, *args, **kw): FieldSet.__init__(self, *args, **kw) self._size = self["entry_length"].value * 8 def createFields(self): yield String(self, "header_id", 4, "DataSet Header Markup (\"mhsd\")", charset="ISO-8859-1") yield UInt32(self, "header_length", "Header Length") yield UInt32(self, "entry_length", "Entry Length") yield Enum(UInt32(self, "type", "type"), self.type_name) padding = self.seekByte(self["header_length"].value, "header_raw") if padding: yield padding if self["type"].value == 1: yield TrackList(self, "tracklist[]") if self["type"].value == 2: yield PlaylistList(self, "playlist_list[]"); if self["type"].value == 3: yield PlaylistList(self, "podcast_list[]"); if self["type"].value == 4: yield AlbumList(self, "album_list[]"); padding = self.seekBit(self._size, "entry padding") if padding: yield padding class DataBase(FieldSet): def __init__(self, *args, **kw): FieldSet.__init__(self, *args, **kw) self._size = self["entry_length"].value * 8 # def createFields(self): class ITunesDBFile(Parser): PARSER_TAGS = { "id": "itunesdb", "category": "audio", "min_size": 44 * 8, "magic": (('mhbd', 0),), "description": "iPod iTunesDB file" } endian = LITTLE_ENDIAN def validate(self): return self.stream.readBytes(0, 4) == 'mhbd' def createFields(self): yield String(self, "header_id", 4, "DataBase Header Markup (\"mhbd\")", charset="ISO-8859-1") yield UInt32(self, "header_length", "Header Length") yield UInt32(self, "entry_length", "Entry Length") yield UInt32(self, "unknown[]") yield UInt32(self, "version_number", "Version Number") yield UInt32(self, "child_number", "Number of Children") yield UInt64(self, "id", "ID for this database") yield UInt16(self, "unknown[]") yield UInt32(self, "unknown[]") yield UInt64(self, "unknown[]") yield UInt16(self, "unknown[]") yield UInt16(self, "hashing_scheme[]", "Algorithm used to calculate the database hash") yield NullBytes(self, "unknown[]", 20) yield String(self, "language_id", 2, "Language ID") yield UInt64(self, "persistent_id", "Library Persistent ID") yield UInt32(self, "unknown[]") yield UInt32(self, "unknown[]") yield RawBytes(self, "hash[]", 20) yield Int32(self, "timezone_offset[]", "Timezone offset in seconds") yield UInt16(self, "unknown[]") yield RawBytes(self, "iphone_hash[]", 45) size = self["header_length"].value - self.current_size / 8 if size > 0: yield NullBytes(self, "padding", size) for i in xrange(self["child_number"].value): yield DataSet(self, "dataset[]") padding = self.seekByte(self["entry_length"].value, "entry padding") if padding: yield padding def createContentSize(self): return self["entry_length"].value * 8