apple-music 0.11.5

A Rust Library to fully control local MacOS Apple Music player.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
use crate::error::Error;
use crate::script_controller::{ParamType, ScriptController};
use serde::Deserialize;
use std::io::Read;
use urlencoding::encode;

/// Provides data related to a specific Track as well as its artworks.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Track {
    /// The class of the Track
    pub class: TrackKind,

    /// The id of the item
    pub id: i32,

    /// The index of the item in internal application order
    pub index: i32,

    /// The name of the item
    pub name: String,

    /// The id of the item as a hexadecimal string. This id does not change over time.
    #[serde(rename = "persistentID")]
    pub persistent_id: String,

    /// The album name of the track
    pub album: String,

    /// The album artist of the track
    pub album_artist: String,

    /// Is the album for this track disliked?
    pub album_disliked: bool,

    /// Is the album for this track favorited?
    pub album_favorited: bool,

    /// The rating of the album for this track (0 to 100)
    pub album_rating: Option<i16>,

    /// The rating kind of the album rating for this track
    pub album_rating_kind: Option<Kind>,

    /// The raw data of the artworks attached to this track.
    pub artworks_raw_data: Option<Vec<Artwork>>,

    /// The URL of the main artwork for this track.
    artwork_url: Option<String>,

    /// The artist of the track
    pub artist: String,

    /// The bit rate of the track (in kbps)
    pub bit_rate: Option<i16>,

    /// The bookmark time of the track in seconds
    pub bookmark: i8,

    /// Is the playback position for this track remembered?
    pub bookmarkable: bool,

    /// The tempo of this track in beats per minute
    pub bpm: i16,

    /// The category of the track
    pub category: String,

    /// The iCloud status of the track
    pub cloud_status: Option<CloudStatus>,

    /// Freeform notes about the track
    pub comment: String,

    /// Is this track from a compilation album?
    pub compilation: bool,

    /// The composer of the track
    pub composer: String,

    /// The common, unique ID for this track. If two tracks in different playlists have the same database ID, they are sharing the same data.
    #[serde(rename = "databaseID")]
    pub database_id: i32,

    /// The date the track was added to the playlist
    pub date_added: String,

    /// The description of the track
    pub description: String,

    /// Is this track disliked?
    pub disliked: bool,

    /// The Apple ID of the person who downloaded this track
    #[serde(rename = "downloaderAppleID")]
    pub downloader_apple_id: Option<String>,

    /// The name of the person who downloaded this track
    pub downloader_name: Option<String>,

    /// The length of the track in seconds
    pub duration: f64,

    /// Is this track checked for playback?
    pub enabled: bool,

    /// The episode ID of the track
    #[serde(rename = "episodeID")]
    pub episode_id: Option<String>,

    /// The episode number of the track
    pub episode_number: i16,

    /// The name of the EQ preset of the track
    pub eq: String,

    /// The stop time of the track in seconds
    pub finish: f64,

    /// Is this track from a gapless album?
    pub gapless: Option<bool>,

    /// The music/audio genre (category) of the track
    pub genre: String,

    /// The grouping (piece) of the track. Generally used to denote movements within a classical work.
    pub grouping: String,

    /// A text description of the track
    pub kind: Option<String>,

    /// The long description of the track
    pub long_description: Option<String>,

    /// Is this track favorited?
    pub favorited: bool,

    /// The lyrics of the track
    pub lyrics: Option<String>,

    /// The media kind of the track
    pub media_kind: MediaKind,

    /// The modification date of the content of this track
    pub modification_date: Option<String>,

    /// The movement name of the track
    pub movement: Option<String>,

    /// The total number of movements in the work
    pub movement_count: i16,

    /// The index of the movement in the work
    pub movement_number: i16,

    /// Number of times this track has been played
    pub played_count: i16,

    /// The date and time this track was last played
    pub played_date: Option<String>,

    /// The Apple ID of the person who purchased this track
    #[serde(rename = "purchaserAppleID")]
    pub purchaser_apple_id: Option<String>,

    /// The name of the person who purchased this track
    pub purchaser_name: Option<String>,

    /// The rating of this track (0 to 100)
    pub rating: i16,

    /// The rating kind of this track
    pub rating_kind: Option<Kind>,

    /// The release date of this track
    pub release_date: Option<String>,

    /// The sample rate of the track (in Hz)
    pub sample_rate: Option<i32>,

    /// The season number of the track
    pub season_number: Option<i16>,

    /// Is this track included when shuffling?
    pub shufflable: bool,

    /// Number of times this track has been skipped
    pub skipped_count: i16,

    /// The date and time this track was last skipped
    pub skipped_date: Option<String>,

    /// The show name of the track
    pub show: Option<String>,

    /// Override string to use for the track when sorting by album
    pub sort_album: Option<String>,

    /// Override string to use for the track when sorting by artist
    pub sort_artist: Option<String>,

    /// Override string to use for the track when sorting by album artist
    pub sort_album_artist: Option<String>,

    /// Override string to use for the track when sorting by name
    pub sort_name: Option<String>,

    /// Override string to use for the track when sorting by composer
    pub sort_composer: Option<String>,

    /// Override string to use for the track when sorting by show name
    pub sort_show: Option<String>,

    /// The size of the track (in bytes)
    pub size: Option<i64>,

    /// The start time of the track in seconds
    pub start: f64,

    /// The length of the track in MM:SS format
    pub time: String,

    /// The total number of tracks on the source album
    pub track_count: i16,

    /// The Apple Music URL for this Track.
    track_url: Option<String>,

    /// The index of the track on the source album
    pub track_number: i16,

    /// Is this track unplayed?
    pub unplayed: bool,

    /// Relative volume adjustment of the track (-100% to 100%)
    pub volume_adjustment: i16,

    /// The work name of the track
    pub work: Option<String>,

    /// The year the track was recorded/released
    pub year: i16,
}

impl Track {
    /// Method that either returns an already fetched artwork_url, or fetches it and then returns it.
    pub fn artwork_url(&mut self) -> &Option<String> {
        if self.artwork_url == None {
            self.fetch_itunes_store_data()
        }

        return &self.artwork_url;
    }

    /// Method that either returns an already fetched track_url, or fetches it and then returns it.
    pub fn track_url(&mut self) -> &Option<String> {
        if self.track_url == None {
            self.fetch_itunes_store_data()
        }

        return &self.track_url;
    }

    /// Returns a list of all artworks with their raw_data.
    /// Recommended to use Track.get_artwork_url() instead.
    pub fn fetch_artworks_raw_data(&mut self) -> Result<(), Error> {
        match ScriptController.execute_script::<Vec<Artwork>>(
            ParamType::Artworks,
            Some(self.id),
            None,
        ) {
            Ok(data) => {
                self.artworks_raw_data = Some(data);
                Ok(())
            }
            Err(err) => Err(err),
        }
    }

    /// Reveals and selects Track in Apple Music.
    pub fn reveal_in_player(&self) -> Result<(), Error> {
        let cmd = format!(
            "Application('Music').reveal(Application('Music').tracks.byId({}))",
            self.id
        );

        let _ = ScriptController.execute(cmd.as_str(), None);

        Ok(())
    }

    /// Triggers a download on Apple Music Player for the Track.
    pub fn download(&self) -> Result<(), Error> {
        let cmd = format!(
            "Application('Music').download(Application('Music').tracks.byId({}))",
            self.id
        );

        let _ = ScriptController.execute(cmd.as_str(), None);

        Ok(())
    }

    /// Favorites / "Unfavorites" a Track.
    pub fn set_favorited(&self, value: bool) -> Result<(), Error> {
        let cmd = format!(
            "Application('Music').tracks.byId({}).favorited = {}",
            self.id, value
        );

        let _ = ScriptController.execute(cmd.as_str(), None);

        Ok(())
    }

    /// Dislikes / "Undislikes" a Track.
    pub fn set_disliked(&self, value: bool) -> Result<(), Error> {
        let cmd = format!(
            "Application('Music').tracks.byId({}).disliked = {}",
            self.id, value
        );

        let _ = ScriptController.execute(cmd.as_str(), None);

        Ok(())
    }

    /// Search for a song in the Itunes Store and extract its artwork_url & track_url.
    fn fetch_itunes_store_data(&mut self) {
        let request = format!(
            "https://itunes.apple.com/search?term={}&entity=song&limit=200",
            encode(self.name.as_str())
        );
        self.fetch_itunes_store_by_request(request);

        if self.artwork_url == None {
            let request = format!(
                "https://itunes.apple.com/search?term={}&entity=song&attribute=albumTerm&limit=200",
                encode(self.album.as_str())
            );
            self.fetch_itunes_store_by_request(request);
        }

        if self.artwork_url == None {
            let request = format!(
                "https://itunes.apple.com/search?term={}&entity=song&limit=200",
                encode(self.artist.as_str())
            );
            self.fetch_itunes_store_by_request(request);
        }
    }

    fn fetch_itunes_store_by_request(&mut self, request: String) {
        let mut res = reqwest::blocking::get(request).unwrap();
        let mut body = String::new();
        res.read_to_string(&mut body).unwrap();

        if let Ok(search) = serde_json::from_str::<ITunesStoreSearch>(body.as_str()) {
            if search.result_count == 1 {
                self.artwork_url = Some(search.results[0].clone().artwork_url_100);
                self.track_url = Some(search.results[0].clone().track_view_url);
            } else {
                let result = search.results.iter().find(|result| {
                    (&result.track_name.to_lowercase() == &self.name.to_lowercase()
                        || &result.track_censored_name.to_lowercase() == &self.name.to_lowercase())
                        && (&result.artist_name.to_lowercase() == &self.artist.to_lowercase()
                            || &result.collection_name.to_lowercase() == &self.album.to_lowercase())
                });

                match result {
                    Some(data) => {
                        self.artwork_url = Some(data.clone().artwork_url_100);
                        self.track_url = Some(data.clone().track_view_url);
                    }
                    None => (),
                }
            }
        }
    }
}

/// Data for a given Artwork.
#[derive(Deserialize, Debug)]
pub struct Artwork {
    /// The class of the item.
    pub class: String,

    /// Data for the artwork, in the form of a picture.
    pub data: Option<String>,

    /// Description of artwork as a string.
    pub description: Option<String>,

    /// Was this artwork downloaded by Apple Music ?
    pub downloaded: bool,

    /// The data format for this piece of artwork.
    pub format: Option<String>,

    /// Kind or purpose of this piece of artwork.
    pub kind: i32,

    /// Data for this artwork, in original format.
    pub raw_data: String,
}

/// Struct representing a search through the Itunes Store.
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ITunesStoreSearch {
    pub result_count: i32,
    pub results: Vec<ITunesStoreData>,
}

/// Struct representing an object from the Itunes Store.
#[derive(Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
struct ITunesStoreData {
    /// The Artist of the Track
    pub artist_name: String,

    /// The censored name of the Track.
    pub track_censored_name: String,

    /// The name of the Track.
    pub track_name: String,

    /// The URL of the main artwork for this track.
    pub artwork_url_100: String,

    /// The Apple Music URL this track.
    pub track_view_url: String,

    /// The Album name of this Track.
    pub collection_name: String,
}

/// Type of Rating: User-made or Computed.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum Kind {
    User,
    Computed,
}

/// iCloud status for Track.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum CloudStatus {
    Unknown,
    Purchased,
    Matched,
    Uploaded,
    Ineligible,
    Removed,
    Error,
    Duplicate,
    Subscription,
    Prerelease,

    #[serde(rename = "no longer available")]
    NoLongerAvailable,
    NotUploaded,
}

/// Type of Media: Song, MusicVideo or Unknown.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub enum MediaKind {
    Song,
    #[serde(rename = "music video")]
    MusicVideo,
    Unknown,
}

/// Type of Track: From an URL, a File, or Shared.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub enum TrackKind {
    SharedTrack,
    FileTrack,
    UrlTrack,
}