modified: Cargo.lock modified: Cargo.toml modified: src/main.rs modified: src/xml.rs
377 lines
13 KiB
Rust
377 lines
13 KiB
Rust
use std::{fs::File, io::{Read, Write}};
|
|
use env_logger::Builder;
|
|
use log::{error, info, LevelFilter};
|
|
use serde::{Deserialize, Serialize};
|
|
use xml::{XAlbumItem, XArgument, XDataSet, XDatabase, XPlaylist, XSomeList, XTrackItem};
|
|
|
|
mod xml;
|
|
|
|
enum ChunkType {
|
|
Database,
|
|
DataSet,
|
|
AlbumList,
|
|
AlbumItem,
|
|
TrackList,
|
|
TrackItem,
|
|
StringTypes,
|
|
PlaylistList,
|
|
Playlist,
|
|
Unknown
|
|
}
|
|
|
|
impl From<[u8; 4]> for ChunkType {
|
|
fn from(value: [u8; 4]) -> Self {
|
|
match value {
|
|
[0x6D, 0x68, 0x62, 0x64] => ChunkType::Database,
|
|
[0x6D, 0x68, 0x73, 0x64] => ChunkType::DataSet,
|
|
[0x6D, 0x68, 0x69, 0x61] => ChunkType::AlbumList,
|
|
[0x6D, 0x68, 0x6C, 0x61] => ChunkType::AlbumItem,
|
|
[0x6D, 0x68, 0x6C, 0x74] => ChunkType::TrackList,
|
|
[0x6D, 0x68, 0x69, 0x74] => ChunkType::TrackItem,
|
|
[0x6D, 0x68, 0x6F, 0x64] => ChunkType::StringTypes,
|
|
[0x6D, 0x68, 0x6C, 0x70] => ChunkType::PlaylistList,
|
|
[0x6D, 0x68, 0x79, 0x70] => ChunkType::Playlist,
|
|
_ => ChunkType::Unknown
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct ChunkHeader {
|
|
chunk_type: [u8; 4],
|
|
end_of_chunk: u32,
|
|
children_count: u32
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct Database {
|
|
unknown: u32,
|
|
version: u32,
|
|
children_count: u32,
|
|
id: u64,
|
|
unknown1: [u8; 32],
|
|
language: u16,
|
|
persistent_id: u64,
|
|
hash: [u8; 20]
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
|
struct DataSet {
|
|
data_type: u32
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct AlbumItem {
|
|
number_of_strings: u32,
|
|
unknown: u16,
|
|
album_id_for_track: u16,
|
|
timestamp: u64,
|
|
unknown1: u32
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct TrackItem {
|
|
number_of_strings: u32, // number of mhod's count
|
|
unique_id: u32,
|
|
visible: u32,
|
|
filetype: u32,
|
|
type1: u8,
|
|
type2: u8,
|
|
compilation_flag: u8,
|
|
stars: u8,
|
|
last_modified_time: u32,
|
|
size: u32,
|
|
length: u32,
|
|
track_number: u32,
|
|
total_tracks: u32,
|
|
year: u32,
|
|
bitrate: u32,
|
|
sample_rate: u32,
|
|
volume: u32,
|
|
start_time: u32,
|
|
stop_time: u32,
|
|
soundcheck: u32,
|
|
play_count: u32,
|
|
play_count2: u32,
|
|
last_played_time: u32,
|
|
disc_number: u32,
|
|
total_discs: u32,
|
|
userid: u32,
|
|
date_added: u32,
|
|
bookmark_time: u32,
|
|
dbid: u64,
|
|
checked: u8,
|
|
application_rating: u8,
|
|
bpm: u16,
|
|
artwork_count: u16,
|
|
unk9: u16,
|
|
artwork_size: u32,
|
|
unk11: u32,
|
|
sample_rate2: u32,
|
|
date_released: u32,
|
|
unk14: u32,
|
|
unk15: u32,
|
|
unk16: u32,
|
|
skip_count: u32,
|
|
last_skipped: u32,
|
|
has_artwork: u8,
|
|
skip_when_shuffling: u8,
|
|
remember_playback_position: u8,
|
|
flag4: u8,
|
|
dbid2: u64,
|
|
lyrics_flag: u8,
|
|
movie_file_flag: u8,
|
|
played_mark: u8,
|
|
unk17: u8,
|
|
unk21: u32,
|
|
pregap: u32,
|
|
sample_count: u64,
|
|
unk25: u32,
|
|
postgap: u32,
|
|
unk27: u32,
|
|
media_type: u32,
|
|
season_number: u32,
|
|
episode_number: u32,
|
|
unk31: [u8; 28],
|
|
gapless_data: u32,
|
|
unk38: u32,
|
|
gapless_track_flag: u16,
|
|
gapless_album_flag: u16,
|
|
unk39_hash: [u8; 20],
|
|
unk40: [u8; 18],
|
|
album_id: u16,
|
|
mhii_link: u32
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct StringEntry { // mhod
|
|
entry_type: u32,
|
|
unk1: u32,
|
|
unk2: u32,
|
|
position: u32,
|
|
length: u32,
|
|
unknown: u32,
|
|
unk4: u32
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct PlaylistIndexEntry { // mhod
|
|
entry_type: u32,
|
|
unk1: u32,
|
|
unk2: u32,
|
|
index_type: u32,
|
|
count: u32,
|
|
null_padding: u64,
|
|
null_padding1: u64,
|
|
null_padding2: u64,
|
|
null_padding3: u64,
|
|
null_padding4: u64
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct LetterJumpEntry {
|
|
entry_type: u32,
|
|
unk1: u32,
|
|
unk2: u32,
|
|
index_type: u32,
|
|
count: u32,
|
|
null_padding: u64
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct JumpTable {
|
|
letter: u32, // UTF-16 LE Uppercase with two padding null bytes
|
|
entry_num: u32, // the number of the first entry in the corresponding MHOD52 index starting with this letter. Zero-based and incremented by one for each entry, not 4.
|
|
count: u32 // the count of entries starting with this letter in the corresponding MHOD52.
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct Playlist {
|
|
data_object_child_count: u32,
|
|
playlist_item_count: u32,
|
|
is_master_playlist_flag: u8,
|
|
unk: [u8; 3],
|
|
timestamp: u32,
|
|
persistent_playlist_id: u64,
|
|
unk3: u32,
|
|
string_mhod_count: u16,
|
|
podcast_flag: u16,
|
|
list_sort_order: u32
|
|
}
|
|
|
|
enum ChunkState {
|
|
Header,
|
|
Data
|
|
}
|
|
|
|
pub fn parse_bytes(data: &[u8]) -> XDatabase {
|
|
let mut xdb = XDatabase{data: None, children: Vec::new()};
|
|
let mut state = ChunkState::Header;
|
|
let mut chunk_header: Option<ChunkHeader> = None;
|
|
let mut last_type: u32 = 0;
|
|
let mut i = 0;
|
|
while i < data.len() {
|
|
state = match state {
|
|
ChunkState::Header => {
|
|
if i + 12 >= data.len() { break; }
|
|
chunk_header = Some(bincode::deserialize(&data[i..i+12]).unwrap());
|
|
i += 12;
|
|
ChunkState::Data
|
|
},
|
|
ChunkState::Data => {
|
|
let mut u = 0;
|
|
let header = chunk_header.unwrap();
|
|
match ChunkType::from(header.chunk_type) {
|
|
ChunkType::Database => {
|
|
info!("Db header: {:?}", header);
|
|
u = usize::try_from(header.end_of_chunk).unwrap() - 12;
|
|
let db: Database = bincode::deserialize(&data[i..i+u]).unwrap();
|
|
info!("val: {:?}", db);
|
|
xdb.data = Some(db);
|
|
},
|
|
ChunkType::DataSet => {
|
|
u = usize::try_from(header.end_of_chunk).unwrap() - 12;
|
|
let ds: DataSet = bincode::deserialize(&data[i..i+u]).unwrap();
|
|
info!("val: {:?}", ds);
|
|
xdb.children.push(XDataSet { data: ds.clone(), child: match ds.data_type {
|
|
3 => XSomeList::Playlists(Vec::new()), // Playlist List
|
|
4 => XSomeList::AlbumList(Vec::new()), // Album List
|
|
_ => XSomeList::TrackList(Vec::new()) // 1 Track List
|
|
}});
|
|
},
|
|
ChunkType::AlbumList => {
|
|
info!("AlbumList");
|
|
u = usize::try_from(header.end_of_chunk).unwrap() - 12;
|
|
last_type = 4;
|
|
},
|
|
ChunkType::AlbumItem => {
|
|
u = usize::try_from(header.end_of_chunk).unwrap() - 12;
|
|
let ai: AlbumItem = bincode::deserialize(&data[i..i+u]).unwrap();
|
|
info!("val: {:?}", ai);
|
|
if let XSomeList::AlbumList(albums) = &mut xdb.find_dataset(4).child {
|
|
albums.push(XAlbumItem {data: ai,args: Vec::new()});
|
|
}
|
|
},
|
|
ChunkType::TrackList => {
|
|
info!("TrackList");
|
|
u = usize::try_from(header.end_of_chunk).unwrap() - 12;
|
|
last_type = 1;
|
|
},
|
|
ChunkType::TrackItem => {
|
|
u = usize::try_from(header.end_of_chunk).unwrap() - 12;
|
|
let ti: TrackItem = bincode::deserialize(&data[i..i+u]).unwrap();
|
|
info!("val: {:?}", ti);
|
|
if let XSomeList::TrackList(tracks) = &mut xdb.find_dataset(1).child {
|
|
tracks.push(XTrackItem {data: ti,args: Vec::new()});
|
|
}
|
|
},
|
|
ChunkType::StringTypes => {
|
|
u = usize::try_from(header.children_count).unwrap() - 12;
|
|
let header_offset: usize = (header.end_of_chunk + 4) as usize;
|
|
let entry_type = u32::from_le_bytes(data[i..i+4].try_into().unwrap());
|
|
match entry_type {
|
|
0..=15 => {
|
|
let str_end: usize = (header.children_count - 12) as usize;
|
|
let entry: StringEntry = bincode::deserialize(&data[i..i+28]).unwrap();
|
|
info!("val: {:?}", &entry);
|
|
let mut bytes = Vec::new();
|
|
|
|
let mut h = i+header_offset;
|
|
while h < i+str_end {
|
|
if data[h] != 0 {
|
|
bytes.push(data[h]);
|
|
}
|
|
h+=1;
|
|
}
|
|
let g = String::from_utf8(bytes).unwrap();
|
|
info!("str: {}", g);
|
|
match &mut xdb.find_dataset(last_type).child {
|
|
XSomeList::AlbumList(albums) => {
|
|
albums.last_mut().unwrap().args.push(XArgument{ arg_type: entry_type, val: g});
|
|
},
|
|
XSomeList::Playlists(playlists) => {
|
|
playlists.last_mut().unwrap().args.push(XArgument{ arg_type: entry_type, val: g});
|
|
},
|
|
XSomeList::TrackList(tracks) => {
|
|
tracks.last_mut().unwrap().args.push(XArgument{ arg_type: entry_type, val: g});
|
|
}
|
|
}
|
|
},
|
|
52 => {
|
|
let entry: PlaylistIndexEntry = bincode::deserialize(&data[i..i+60]).unwrap();
|
|
info!("valPl: {:?}", &entry);
|
|
let mut h = i+60;
|
|
let mut v = Vec::new();
|
|
while h < i+60+((4*entry.count) as usize) {
|
|
v.push(u32::from_le_bytes(data[h..h+4].try_into().unwrap()));
|
|
h += 4;
|
|
}
|
|
info!("Indexes: {:?}", v);
|
|
},
|
|
53 => {
|
|
let entry: LetterJumpEntry = bincode::deserialize(&data[i..i+28]).unwrap();
|
|
info!("valJT: {:?}", &entry);
|
|
let mut h = i+28;
|
|
let mut v: Vec<JumpTable> = Vec::new();
|
|
while h < i+28+((12*entry.count) as usize) {
|
|
v.push(bincode::deserialize(&data[h..h+12]).unwrap());
|
|
h += 12;
|
|
}
|
|
info!("Indexes: {:?}", v);
|
|
},
|
|
100 => {
|
|
|
|
},
|
|
102 => {
|
|
|
|
},
|
|
_ => {}
|
|
}
|
|
},
|
|
ChunkType::PlaylistList => {
|
|
info!("Playlists count: {}", header.children_count);
|
|
u = usize::try_from(header.end_of_chunk).unwrap() - 12;
|
|
last_type = 3;
|
|
},
|
|
ChunkType::Playlist => {
|
|
u = usize::try_from(header.end_of_chunk).unwrap() - 12;
|
|
let playlist: Playlist = bincode::deserialize(&data[i..i+u]).unwrap();
|
|
info!("playlist: {:?}", playlist);
|
|
if let XSomeList::Playlists(playlists) = &mut xdb.find_dataset(3).child {
|
|
playlists.push(XPlaylist {data: playlist,args: Vec::new()});
|
|
}
|
|
},
|
|
_ => { u = 1; }
|
|
}
|
|
i += u;
|
|
chunk_header = None;
|
|
ChunkState::Header
|
|
}
|
|
}
|
|
}
|
|
//let mut f = File::create("output.json").unwrap();
|
|
//let r = f.write(serde_json::to_string::<XDatabase>(&xdb).unwrap().as_bytes());
|
|
//info!("Result: {:?}", r);
|
|
xdb
|
|
}
|
|
|
|
/*fn main() {
|
|
|
|
// Initialize the logger with 'info' as the default level
|
|
Builder::new()
|
|
.filter(None, LevelFilter::Info)
|
|
.init();
|
|
|
|
let mut f = File::open("D:\\Documents\\iTunes\\iTunesDB").unwrap();
|
|
let mut buf = Vec::new();
|
|
match f.read_to_end(&mut buf) {
|
|
Ok(n) => {
|
|
let data = &buf[..n];
|
|
parse_bytes(data);
|
|
},
|
|
Err(e) => {
|
|
error!("Error: {}",e);
|
|
}
|
|
}
|
|
}*/
|