Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/cs/support apple music lyrics #445

Merged
merged 6 commits into from
Jan 25, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 25 additions & 45 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion imessage-database/Cargo.toml
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ version = "0.0.0"
[dependencies]
chrono = "=0.4.39"
plist = "=1.7.0"
rusqlite = { version = "=0.32.1", features = ["blob", "bundled"] }
rusqlite = { version = "=0.33.0", features = ["blob", "bundled"] }
sha1 = "=0.10.6"
protobuf = "=3.7.1"
lzma-rs = "=0.3.0"
51 changes: 41 additions & 10 deletions imessage-database/src/message_types/music.rs
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ use plist::Value;
use crate::{
error::plist::PlistParseError,
message_types::variants::BalloonProvider,
util::plist::{get_string_from_dict, get_string_from_nested_dict},
util::plist::{get_string_from_dict, get_string_from_nested_dict, get_value_from_dict},
};

/// This struct is not documented by Apple, but represents messages displayed as
@@ -24,6 +24,8 @@ pub struct MusicMessage<'a> {
pub album: Option<&'a str>,
/// Track name
pub track_name: Option<&'a str>,
/// Included lyrics, if any
pub lyrics: Option<Vec<&'a str>>,
}

impl<'a> BalloonProvider<'a> for MusicMessage<'a> {
@@ -40,6 +42,9 @@ impl<'a> BalloonProvider<'a> for MusicMessage<'a> {
artist: get_string_from_dict(music_metadata, "artist"),
album: get_string_from_dict(music_metadata, "album"),
track_name: get_string_from_dict(music_metadata, "name"),
lyrics: get_value_from_dict(music_metadata, "lyricExcerpt")
.and_then(|l| get_string_from_dict(l, "lyrics"))
.map(|lyrics| lyrics.split("\n").collect()),
});
}
Err(PlistParseError::NoPayload)
@@ -49,23 +54,21 @@ impl<'a> BalloonProvider<'a> for MusicMessage<'a> {
impl<'a> MusicMessage<'a> {
/// Extract the main dictionary of data from the body of the payload
///
/// Apple Music stores the URL under `richLinkMetadata` like a normal URL, but has some
/// Apple Music stores the URL under `richLinkMetadata` like a normal URL, but has some
/// extra data stored under `specialization` that contains the track information.
fn get_body_and_url(payload: &'a Value) -> Result<(&'a Value, &'a Value), PlistParseError> {
let base = payload
.as_dictionary()
.ok_or_else(|| PlistParseError::InvalidType(
"root".to_string(),
"dictionary".to_string(),
))?
.ok_or_else(|| {
PlistParseError::InvalidType("root".to_string(), "dictionary".to_string())
})?
.get("richLinkMetadata")
.ok_or_else(|| PlistParseError::MissingKey("richLinkMetadata".to_string()))?;
Ok((
base.as_dictionary()
.ok_or_else(|| PlistParseError::InvalidType(
"root".to_string(),
"dictionary".to_string(),
))?
.ok_or_else(|| {
PlistParseError::InvalidType("root".to_string(), "dictionary".to_string())
})?
.get("specialization")
.ok_or_else(|| PlistParseError::MissingKey("specialization".to_string()))?,
base,
@@ -102,6 +105,34 @@ mod tests {
artist: Some("БАТЮШКА"),
album: Some("Панихида"),
track_name: Some("Песнь 1"),
lyrics: None,
};

assert_eq!(balloon, expected);
}

#[test]
fn test_parse_apple_music_lyrics() {
let plist_path = current_dir()
.unwrap()
.as_path()
.join("test_data/music_message/AppleMusicLyrics.plist");
let plist_data = File::open(plist_path).unwrap();
let plist = Value::from_reader(plist_data).unwrap();
let parsed = parse_ns_keyed_archiver(&plist).unwrap();

println!("{:#?}", parsed);

let balloon = MusicMessage::from_map(&parsed).unwrap();
let expected = MusicMessage {
url: Some(
"https://music.apple.com/us/lyrics/1329891623?ts=11.108&te=16.031&l=en&tk=2.v1.VsuX9f%2BaT1PyrgMgIT7ANQ%3D%3D&itsct=sharing_msg_lyrics&itscg=50401",
),
preview: None,
artist: Some("Dual Core"),
album: Some("Downtime"),
track_name: Some("Another Chapter"),
lyrics: Some(vec!["I remember when it all started, something from a dream", "Addicted to the black and green letters on my screen"]),
};

assert_eq!(balloon, expected);
41 changes: 41 additions & 0 deletions imessage-database/src/tables/messages/body.rs
Original file line number Diff line number Diff line change
@@ -1101,6 +1101,47 @@ mod typedstream_tests {
}),]
);
}

#[test]
fn can_get_message_body_apple_music_lyrics() {
let mut m = Message::blank();
m.text = Some("\u{FFFC}".to_string());

let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/AppleMusicLyrics");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();

let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();

parse_body_typedstream(
m.components.as_ref(),
m.text.as_deref(),
m.edited_parts.as_ref(),
)
.unwrap()
.iter()
.enumerate()
.for_each(|(idx, item)| println!("\t{idx}: {item:#?}"));

assert_eq!(
parse_body_typedstream(
m.components.as_ref(),
m.text.as_deref(),
m.edited_parts.as_ref()
)
.unwrap(),
vec![BubbleComponent::Text(vec![TextAttributes::new(
0,
3,
TextEffect::Link("https://music.apple.com/us/lyrics/1329891623?ts=11.108&te=16.031&l=en&tk=2.v1.VsuX9f%2BaT1PyrgMgIT7ANQ%3D%3D&itsct=sharing_msg_lyrics&itscg=50401")
),])]
);
}
}

#[cfg(test)]
2 changes: 1 addition & 1 deletion imessage-database/src/tables/table.rs
Original file line number Diff line number Diff line change
@@ -109,7 +109,7 @@ pub fn get_connection(path: &Path) -> Result<Connection, TableError> {
Ok(res) => Ok(res),
Err(why) => Err(
TableError::CannotConnect(
format!("Unable to read from chat database: {why}\nEnsure full disk access is enabled for your terminal emulator in System Settings > Security and Privacy > Full Disk Access")
format!("Unable to read from chat database: {why}\nEnsure full disk access is enabled for your terminal emulator in System Settings > Privacy & Security > Full Disk Access")
)),
};
};
5 changes: 5 additions & 0 deletions imessage-database/src/util/plist.rs
Original file line number Diff line number Diff line change
@@ -232,6 +232,11 @@ pub fn get_string_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a
.filter(|s| !s.is_empty())
}

/// Extract an inner dict from a key-value pair that looks like `{key: {key2: val}}`
pub fn get_value_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a Value> {
payload.as_dictionary()?.get(key)
}

/// Extract a bool from a key-value pair that looks like `{key: true}`
pub fn get_bool_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<bool> {
payload.as_dictionary()?.get(key)?.as_boolean()
Loading