Skip to main content

imessage_database/message_types/
music.rs

1/*!
2 Apple Music link previews stored in URL balloon payloads.
3*/
4
5use plist::Value;
6
7use crate::{
8    error::plist::PlistParseError,
9    message_types::variants::BalloonProvider,
10    util::plist::{
11        get_string_from_dict, get_string_from_nested_dict, get_value_from_dict,
12        rich_link_metadata_and_nested,
13    },
14};
15
16/// This struct is not documented by Apple, but represents messages displayed as
17/// `com.apple.messages.URLBalloonProvider` but from the Music app
18#[derive(Debug, PartialEq, Eq)]
19pub struct MusicMessage<'a> {
20    /// Apple Music URL.
21    pub url: Option<&'a str>,
22    /// Track preview stream URL.
23    pub preview: Option<&'a str>,
24    /// Artist name.
25    pub artist: Option<&'a str>,
26    /// Album name.
27    pub album: Option<&'a str>,
28    /// Track name.
29    pub track_name: Option<&'a str>,
30    /// Included lyric lines.
31    pub lyrics: Option<Vec<&'a str>>,
32}
33
34impl<'a> BalloonProvider<'a> for MusicMessage<'a> {
35    fn from_map(payload: &'a Value) -> Result<Self, PlistParseError> {
36        if let Ok((body, music_metadata)) = rich_link_metadata_and_nested(payload, "specialization")
37        {
38            // Music payloads carry album metadata.
39            if get_string_from_dict(music_metadata, "album").is_none() {
40                return Err(PlistParseError::WrongMessageType);
41            }
42
43            return Ok(Self {
44                url: get_string_from_nested_dict(body, "URL"),
45                preview: get_string_from_nested_dict(music_metadata, "previewURL"),
46                artist: get_string_from_dict(music_metadata, "artist"),
47                album: get_string_from_dict(music_metadata, "album"),
48                track_name: get_string_from_dict(music_metadata, "name"),
49                lyrics: get_value_from_dict(music_metadata, "lyricExcerpt")
50                    .and_then(|l| get_string_from_dict(l, "lyrics"))
51                    .map(|lyrics| lyrics.split('\n').collect()),
52            });
53        }
54        Err(PlistParseError::NoPayload)
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use crate::{
61        message_types::{music::MusicMessage, variants::BalloonProvider},
62        util::plist::parse_ns_keyed_archiver,
63    };
64    use plist::Value;
65    use std::env::current_dir;
66    use std::fs::File;
67
68    #[test]
69    fn test_parse_apple_music() {
70        let plist_path = current_dir()
71            .unwrap()
72            .as_path()
73            .join("test_data/music_message/AppleMusic.plist");
74        let plist_data = File::open(plist_path).unwrap();
75        let plist = Value::from_reader(plist_data).unwrap();
76        let parsed = parse_ns_keyed_archiver(&plist).unwrap();
77
78        let balloon = MusicMessage::from_map(&parsed).unwrap();
79        let expected = MusicMessage {
80            url: Some(
81                "https://music.apple.com/us/album/%D0%BF%D0%B5%D1%81%D0%BD%D1%8C-1/1539641998?i=1539641999",
82            ),
83            preview: Some(
84                "https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview115/v4/b2/65/b3/b265b31f-facb-3ea3-e6bc-91a8d01c9b2f/mzaf_18233159060539450284.plus.aac.ep.m4a",
85            ),
86            artist: Some("БАТЮШКА"),
87            album: Some("Панихида"),
88            track_name: Some("Песнь 1"),
89            lyrics: None,
90        };
91
92        assert_eq!(balloon, expected);
93    }
94
95    #[test]
96    fn test_parse_apple_music_lyrics() {
97        let plist_path = current_dir()
98            .unwrap()
99            .as_path()
100            .join("test_data/music_message/AppleMusicLyrics.plist");
101        let plist_data = File::open(plist_path).unwrap();
102        let plist = Value::from_reader(plist_data).unwrap();
103        let parsed = parse_ns_keyed_archiver(&plist).unwrap();
104
105        println!("{parsed:#?}");
106
107        let balloon = MusicMessage::from_map(&parsed).unwrap();
108        let expected = MusicMessage {
109            url: Some(
110                "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",
111            ),
112            preview: None,
113            artist: Some("Dual Core"),
114            album: Some("Downtime"),
115            track_name: Some("Another Chapter"),
116            lyrics: Some(vec![
117                "I remember when it all started, something from a dream",
118                "Addicted to the black and green letters on my screen",
119            ]),
120        };
121
122        assert_eq!(balloon, expected);
123    }
124}