cider_api/types.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Types for the Cider REST API.
6//!
7//! This module contains all request and response types used by
8//! [`CiderClient`](crate::CiderClient). Response types use `#[serde(default)]`
9//! on fields that may be absent so deserialization succeeds even when the API
10//! omits them (e.g. radio stations may omit `artist_name`).
11//!
12//! The response shapes match the [Cider RPC documentation](https://cider.sh/docs/client/rpc).
13
14use serde::{Deserialize, Serialize};
15
16// ─── Response wrapper ────────────────────────────────────────────────────────
17
18/// Generic wrapper for Cider API JSON responses.
19///
20/// Most endpoints return `{ "status": "ok", ...fields }`. The inner payload is
21/// flattened so its fields sit alongside `status`.
22///
23/// # Example (JSON)
24///
25/// ```json
26/// { "status": "ok", "is_playing": true }
27/// ```
28#[derive(Debug, Clone, Deserialize)]
29pub struct ApiResponse<T> {
30 /// Status string, typically `"ok"`.
31 pub status: String,
32
33 /// Endpoint-specific payload, flattened into the same JSON object.
34 #[serde(flatten)]
35 pub data: T,
36}
37
38// ─── Common types ────────────────────────────────────────────────────────────
39
40/// Artwork metadata for a track, album, or station.
41///
42/// The `url` field may contain `{w}` and `{h}` placeholders for the desired
43/// image dimensions. Use [`Artwork::url_for_size`] to get a ready-to-use URL.
44///
45/// Color fields (`text_color1`–`text_color4`, `bg_color`) are hex color strings
46/// present on certain container artwork (e.g. radio stations).
47///
48/// # Examples
49///
50/// ```
51/// # use cider_api::Artwork;
52/// let art = Artwork {
53/// width: 600,
54/// height: 600,
55/// url: "https://example.com/img/{w}x{h}bb.jpg".into(),
56/// ..Default::default()
57/// };
58/// assert_eq!(
59/// art.url_for_size(300),
60/// "https://example.com/img/300x300bb.jpg"
61/// );
62/// ```
63#[derive(Debug, Clone, Default, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct Artwork {
66 /// Image width in pixels.
67 #[serde(default)]
68 pub width: u32,
69
70 /// Image height in pixels.
71 #[serde(default)]
72 pub height: u32,
73
74 /// URL template — may contain `{w}` and `{h}` size placeholders.
75 #[serde(default)]
76 pub url: String,
77
78 /// Primary text color (hex, e.g. `"eaccc1"`). Present on station artwork.
79 #[serde(default)]
80 pub text_color1: Option<String>,
81
82 /// Secondary text color (hex). Present on station artwork.
83 #[serde(default)]
84 pub text_color2: Option<String>,
85
86 /// Tertiary text color (hex). Present on station artwork.
87 #[serde(default)]
88 pub text_color3: Option<String>,
89
90 /// Quaternary text color (hex). Present on station artwork.
91 #[serde(default)]
92 pub text_color4: Option<String>,
93
94 /// Background color (hex, e.g. `"0c0e0d"`). Present on station artwork.
95 #[serde(default)]
96 pub bg_color: Option<String>,
97
98 /// Whether the artwork uses the Display P3 color space.
99 #[serde(default)]
100 pub has_p3: Option<bool>,
101}
102
103impl Artwork {
104 /// Return the artwork URL with `{w}` and `{h}` replaced by `size`.
105 ///
106 /// If the URL has no placeholders the original URL is returned unchanged.
107 #[must_use]
108 pub fn url_for_size(&self, size: u32) -> String {
109 let s = size.to_string();
110 self.url.replace("{w}", &s).replace("{h}", &s)
111 }
112}
113
114/// Play parameters identifying a playable item.
115///
116/// Every playable track, album, or station carries an `id` (Apple Music
117/// catalog ID) and a `kind` (e.g. `"song"`, `"album"`, `"radioStation"`).
118///
119/// # Examples
120///
121/// ```
122/// # use cider_api::PlayParams;
123/// let pp = PlayParams { id: "1719861213".into(), kind: "song".into() };
124/// assert_eq!(pp.id, "1719861213");
125/// ```
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct PlayParams {
128 /// Apple Music catalog ID.
129 pub id: String,
130
131 /// Item kind — `"song"`, `"album"`, `"playlist"`, `"radioStation"`, etc.
132 pub kind: String,
133}
134
135/// A track audio preview.
136///
137/// The `url` points to a short AAC preview clip hosted on Apple's CDN.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct Preview {
140 /// Direct URL to the preview audio file.
141 pub url: String,
142}
143
144// ─── Now Playing ─────────────────────────────────────────────────────────────
145
146/// Currently playing track information returned by `GET /now-playing`.
147///
148/// This is an Apple Music API–style resource enriched with live playback
149/// state (`current_playback_time`, `remaining_time`, `shuffle_mode`, etc.).
150///
151/// All fields use `#[serde(default)]` so deserialization succeeds even when
152/// the API omits fields (e.g. radio stations may lack `artist_name`).
153///
154/// # Examples
155///
156/// ```
157/// # use cider_api::NowPlaying;
158/// # fn example(track: &NowPlaying) {
159/// println!("{} — {} ({})", track.name, track.artist_name, track.album_name);
160/// println!("Position: {:.1}s / {}ms", track.current_playback_time, track.duration_in_millis);
161/// if let Some(id) = track.song_id() {
162/// println!("Song ID: {id}");
163/// }
164/// println!("Artwork: {}", track.artwork_url(600));
165/// # }
166/// ```
167#[derive(Debug, Clone, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169#[allow(clippy::struct_excessive_bools)]
170pub struct NowPlaying {
171 /// Song name.
172 #[serde(default)]
173 pub name: String,
174
175 /// Artist name.
176 #[serde(default)]
177 pub artist_name: String,
178
179 /// Album name.
180 #[serde(default)]
181 pub album_name: String,
182
183 /// Artwork information.
184 #[serde(default)]
185 pub artwork: Artwork,
186
187 /// Total duration in milliseconds.
188 #[serde(default)]
189 pub duration_in_millis: u64,
190
191 // ── Identifiers ──
192
193 /// Play parameters containing the song ID and kind.
194 #[serde(default)]
195 pub play_params: Option<PlayParams>,
196
197 /// Apple Music web URL for the track.
198 #[serde(default)]
199 pub url: Option<String>,
200
201 /// International Standard Recording Code.
202 #[serde(default)]
203 pub isrc: Option<String>,
204
205 // ── Playback state (injected by Cider, not in the Apple Music catalog) ──
206
207 /// Current playback position in seconds.
208 #[serde(default)]
209 pub current_playback_time: f64,
210
211 /// Remaining playback time in seconds.
212 #[serde(default)]
213 pub remaining_time: f64,
214
215 /// Shuffle mode — `0` = off, `1` = on.
216 #[serde(default)]
217 pub shuffle_mode: u8,
218
219 /// Repeat mode — `0` = off, `1` = repeat one, `2` = repeat all.
220 #[serde(default)]
221 pub repeat_mode: u8,
222
223 /// Whether the track is in the user's favorites.
224 #[serde(default)]
225 pub in_favorites: bool,
226
227 /// Whether the track is in the user's library.
228 #[serde(default)]
229 pub in_library: bool,
230
231 // ── Catalog metadata ──
232
233 /// Genre names (e.g. `["Electronic", "Music"]`).
234 #[serde(default)]
235 pub genre_names: Vec<String>,
236
237 /// Track number on the album.
238 #[serde(default)]
239 pub track_number: u32,
240
241 /// Disc number on the album.
242 #[serde(default)]
243 pub disc_number: u32,
244
245 /// Release date as an ISO-8601 string (e.g. `"2016-05-27T12:00:00Z"`).
246 #[serde(default)]
247 pub release_date: Option<String>,
248
249 /// Audio locale code (e.g. `"en-US"`).
250 #[serde(default)]
251 pub audio_locale: Option<String>,
252
253 /// Composer / songwriter name.
254 #[serde(default)]
255 pub composer_name: Option<String>,
256
257 /// Whether the track has lyrics.
258 #[serde(default)]
259 pub has_lyrics: bool,
260
261 /// Whether the track has time-synced (karaoke-style) lyrics.
262 #[serde(default)]
263 pub has_time_synced_lyrics: bool,
264
265 /// Whether vocal attenuation (sing-along mode) is available.
266 #[serde(default)]
267 pub is_vocal_attenuation_allowed: bool,
268
269 /// Legacy flag — replaced by [`is_apple_digital_master`](Self::is_apple_digital_master).
270 #[serde(default)]
271 pub is_mastered_for_itunes: bool,
272
273 /// Whether the track is an Apple Digital Master (high-resolution master).
274 #[serde(default)]
275 pub is_apple_digital_master: bool,
276
277 /// Audio traits (e.g. `["atmos", "lossless", "lossy-stereo", "spatial"]`).
278 #[serde(default)]
279 pub audio_traits: Vec<String>,
280
281 /// Audio preview URLs.
282 #[serde(default)]
283 pub previews: Vec<Preview>,
284}
285
286impl NowPlaying {
287 /// Get the song ID from [`play_params`](Self::play_params), if present.
288 #[must_use]
289 pub fn song_id(&self) -> Option<&str> {
290 self.play_params.as_ref().map(|p| p.id.as_str())
291 }
292
293 /// Get the current playback position in milliseconds.
294 ///
295 /// Negative `current_playback_time` values (possible at seek boundaries)
296 /// are clamped to zero.
297 #[must_use]
298 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
299 pub fn current_position_ms(&self) -> u64 {
300 // max(0.0) guards against negative values; truncation is intentional.
301 (self.current_playback_time.max(0.0) * 1000.0).round() as u64
302 }
303
304 /// Get the artwork URL at the specified square size (in pixels).
305 ///
306 /// Shorthand for `self.artwork.url_for_size(size)`.
307 #[must_use]
308 pub fn artwork_url(&self, size: u32) -> String {
309 self.artwork.url_for_size(size)
310 }
311}
312
313// ─── Queue types ─────────────────────────────────────────────────────────────
314
315/// A single item in the Cider playback queue.
316///
317/// Returned as part of the array from `GET /queue`. The queue includes
318/// history items, the currently playing track, and upcoming items. Use
319/// [`QueueItem::is_current`] to identify the active track.
320///
321/// Most useful data lives in [`attributes`](Self::attributes). Top-level
322/// fields like `asset_url`, `assets`, and `key_urls` are Apple Music
323/// streaming internals.
324///
325/// # Examples
326///
327/// ```no_run
328/// # use cider_api::{CiderClient, QueueItem};
329/// # async fn example() -> Result<(), cider_api::CiderError> {
330/// let queue = CiderClient::new().get_queue().await?;
331///
332/// // Find the currently playing item
333/// if let Some(current) = queue.iter().find(|i| i.is_current()) {
334/// if let Some(attrs) = ¤t.attributes {
335/// println!("Now playing: {} — {}", attrs.name, attrs.artist_name);
336/// }
337/// }
338///
339/// // List upcoming tracks
340/// let current_idx = queue.iter().position(|i| i.is_current()).unwrap_or(0);
341/// for item in &queue[current_idx + 1..] {
342/// if let Some(attrs) = &item.attributes {
343/// println!(" Up next: {}", attrs.name);
344/// }
345/// }
346/// # Ok(())
347/// # }
348/// ```
349#[derive(Debug, Clone, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct QueueItem {
352 /// Apple Music catalog ID for this item.
353 #[serde(default)]
354 pub id: Option<String>,
355
356 /// Item type (e.g. `"song"`).
357 #[serde(default, rename = "type")]
358 pub item_type: Option<String>,
359
360 /// HLS streaming URL for the asset.
361 #[serde(default, rename = "assetURL")]
362 pub asset_url: Option<String>,
363
364 /// HLS metadata (opaque object).
365 #[serde(default)]
366 pub hls_metadata: Option<serde_json::Value>,
367
368 /// Audio flavor / codec descriptor (e.g. `"28:ctrp256"`).
369 #[serde(default)]
370 pub flavor: Option<String>,
371
372 /// Track metadata attributes.
373 #[serde(default)]
374 pub attributes: Option<QueueItemAttributes>,
375
376 /// Playback type identifier.
377 #[serde(default)]
378 pub playback_type: Option<u32>,
379
380 /// The container this item was queued from (e.g. a station or playlist).
381 #[serde(default, rename = "_container")]
382 pub container: Option<QueueContainer>,
383
384 /// Context information about how this item was queued.
385 #[serde(default, rename = "_context")]
386 pub context: Option<QueueContext>,
387
388 /// Playback state — `current == Some(2)` means currently playing.
389 #[serde(default, rename = "_state")]
390 pub state: Option<QueueItemState>,
391
392 /// Song ID (may differ from `id` for library vs. catalog tracks).
393 #[serde(default, rename = "_songId")]
394 pub song_id: Option<String>,
395
396 /// Available audio assets with different codec flavors and metadata.
397 #[serde(default)]
398 pub assets: Option<Vec<serde_json::Value>>,
399
400 /// DRM key URLs for HLS playback.
401 #[serde(default, rename = "keyURLs")]
402 pub key_urls: Option<KeyUrls>,
403}
404
405impl QueueItem {
406 /// Returns `true` if this is the currently playing item.
407 #[must_use]
408 pub fn is_current(&self) -> bool {
409 self.state
410 .as_ref()
411 .and_then(|s| s.current)
412 .is_some_and(|c| c == 2)
413 }
414}
415
416/// Track attributes within a [`QueueItem`].
417///
418/// Contains the same catalog metadata as [`NowPlaying`] plus
419/// live playback state injected by Cider.
420#[derive(Debug, Clone, Serialize, Deserialize)]
421#[serde(rename_all = "camelCase")]
422#[allow(clippy::struct_excessive_bools)]
423pub struct QueueItemAttributes {
424 /// Song name.
425 #[serde(default)]
426 pub name: String,
427
428 /// Artist name.
429 #[serde(default)]
430 pub artist_name: String,
431
432 /// Album name.
433 #[serde(default)]
434 pub album_name: String,
435
436 /// Total duration in milliseconds.
437 #[serde(default)]
438 pub duration_in_millis: u64,
439
440 // ── Identifiers ──
441
442 /// Artwork information.
443 #[serde(default)]
444 pub artwork: Option<Artwork>,
445
446 /// Play parameters containing the song ID and kind.
447 #[serde(default)]
448 pub play_params: Option<PlayParams>,
449
450 /// Apple Music web URL for the track.
451 #[serde(default)]
452 pub url: Option<String>,
453
454 /// International Standard Recording Code.
455 #[serde(default)]
456 pub isrc: Option<String>,
457
458 // ── Catalog metadata ──
459
460 /// Genre names.
461 #[serde(default)]
462 pub genre_names: Vec<String>,
463
464 /// Track number on the album.
465 #[serde(default)]
466 pub track_number: u32,
467
468 /// Disc number on the album.
469 #[serde(default)]
470 pub disc_number: u32,
471
472 /// Release date as an ISO-8601 string.
473 #[serde(default)]
474 pub release_date: Option<String>,
475
476 /// Audio locale code (e.g. `"en-US"`).
477 #[serde(default)]
478 pub audio_locale: Option<String>,
479
480 /// Composer / songwriter name.
481 #[serde(default)]
482 pub composer_name: Option<String>,
483
484 /// Whether the track has lyrics.
485 #[serde(default)]
486 pub has_lyrics: bool,
487
488 /// Whether the track has time-synced lyrics.
489 #[serde(default)]
490 pub has_time_synced_lyrics: bool,
491
492 /// Whether vocal attenuation is available.
493 #[serde(default)]
494 pub is_vocal_attenuation_allowed: bool,
495
496 /// Legacy Mastered for iTunes flag.
497 #[serde(default)]
498 pub is_mastered_for_itunes: bool,
499
500 /// Whether the track is an Apple Digital Master.
501 #[serde(default)]
502 pub is_apple_digital_master: bool,
503
504 /// Audio traits (e.g. `["lossless", "lossy-stereo"]`).
505 #[serde(default)]
506 pub audio_traits: Vec<String>,
507
508 /// Audio preview URLs.
509 #[serde(default)]
510 pub previews: Vec<Preview>,
511
512 // ── Playback state (injected by Cider) ──
513
514 /// Current playback position in seconds.
515 #[serde(default)]
516 pub current_playback_time: f64,
517
518 /// Remaining playback time in seconds.
519 #[serde(default)]
520 pub remaining_time: f64,
521}
522
523/// Playback state of a [`QueueItem`].
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct QueueItemState {
526 /// `2` indicates this is the currently playing item.
527 #[serde(default)]
528 pub current: Option<u8>,
529}
530
531/// The container (playlist, station, album) a queue item was sourced from.
532///
533/// Container `attributes` vary by type and are exposed as raw JSON.
534#[derive(Debug, Clone, Serialize, Deserialize)]
535#[serde(rename_all = "camelCase")]
536pub struct QueueContainer {
537 /// Container ID (e.g. `"ra.cp-1055074639"`).
538 #[serde(default)]
539 pub id: Option<String>,
540
541 /// Container type (e.g. `"stations"`, `"playlists"`, `"albums"`).
542 #[serde(default, rename = "type")]
543 pub container_type: Option<String>,
544
545 /// Apple Music API href for the container.
546 #[serde(default)]
547 pub href: Option<String>,
548
549 /// Display name / context label (e.g. `"now_playing"`).
550 #[serde(default)]
551 pub name: Option<String>,
552
553 /// Container-specific attributes (varies by type).
554 #[serde(default)]
555 pub attributes: Option<serde_json::Value>,
556}
557
558/// Context metadata for a [`QueueItem`].
559#[derive(Debug, Clone, Serialize, Deserialize)]
560#[serde(rename_all = "camelCase")]
561pub struct QueueContext {
562 /// Feature that queued this item (e.g. `"now_playing"`).
563 #[serde(default)]
564 pub feature_name: Option<String>,
565}
566
567/// DRM / streaming key URLs for HLS playback.
568#[derive(Debug, Clone, Serialize, Deserialize)]
569pub struct KeyUrls {
570 /// URL for the HLS `FairPlay` certificate bundle.
571 #[serde(default, rename = "hls-key-cert-url")]
572 pub hls_key_cert_url: Option<String>,
573
574 /// URL for the HLS `FairPlay` license server.
575 #[serde(default, rename = "hls-key-server-url")]
576 pub hls_key_server_url: Option<String>,
577
578 /// URL for the Widevine certificate.
579 #[serde(default, rename = "widevine-cert-url")]
580 pub widevine_cert_url: Option<String>,
581}
582
583// ─── Endpoint-specific response payloads ─────────────────────────────────────
584
585/// Payload for `GET /is-playing`.
586#[derive(Debug, Clone, Deserialize)]
587pub struct IsPlayingResponse {
588 /// `true` if music is currently playing.
589 pub is_playing: bool,
590}
591
592/// Payload for `GET /now-playing`.
593#[derive(Debug, Clone, Deserialize)]
594pub struct NowPlayingResponse {
595 /// Currently playing track info.
596 pub info: NowPlaying,
597}
598
599/// Payload for `GET /volume`.
600#[derive(Debug, Clone, Deserialize)]
601pub struct VolumeResponse {
602 /// Current volume level (`0.0`–`1.0`).
603 pub volume: f32,
604}
605
606/// Payload for `GET /repeat-mode`.
607#[derive(Debug, Clone, Deserialize)]
608pub struct RepeatModeResponse {
609 /// `0` = off, `1` = repeat one, `2` = repeat all.
610 pub value: u8,
611}
612
613/// Payload for `GET /shuffle-mode`.
614#[derive(Debug, Clone, Deserialize)]
615pub struct ShuffleModeResponse {
616 /// `0` = off, `1` = on.
617 pub value: u8,
618}
619
620/// Payload for `GET /autoplay`.
621#[derive(Debug, Clone, Deserialize)]
622pub struct AutoplayResponse {
623 /// `true` = autoplay enabled.
624 pub value: bool,
625}
626
627// ─── Request bodies ──────────────────────────────────────────────────────────
628
629/// Request body for `POST /play-url`.
630#[derive(Debug, Clone, Serialize)]
631pub struct PlayUrlRequest {
632 /// Apple Music URL to play (e.g. `"https://music.apple.com/…"`).
633 pub url: String,
634}
635
636/// Request body for `POST /play-item` / `POST /play-next` / `POST /play-later`.
637#[derive(Debug, Clone, Serialize)]
638pub struct PlayItemRequest {
639 /// Item type (e.g. `"songs"`, `"albums"`, `"playlists"`).
640 #[serde(rename = "type")]
641 pub item_type: String,
642
643 /// Apple Music catalog ID (must be a string, not a number).
644 pub id: String,
645}
646
647/// Request body for `POST /play-item-href`.
648#[derive(Debug, Clone, Serialize)]
649pub struct PlayItemHrefRequest {
650 /// Apple Music API href (e.g. `"/v1/catalog/ca/songs/1719861213"`).
651 pub href: String,
652}
653
654/// Request body for `POST /seek`.
655#[derive(Debug, Clone, Serialize)]
656pub struct SeekRequest {
657 /// Target position in **seconds**.
658 pub position: f64,
659}
660
661/// Request body for `POST /volume`.
662#[derive(Debug, Clone, Serialize)]
663pub struct VolumeRequest {
664 /// Target volume (`0.0`–`1.0`).
665 pub volume: f32,
666}
667
668/// Request body for `POST /set-rating`.
669#[derive(Debug, Clone, Serialize)]
670pub struct RatingRequest {
671 /// `-1` = dislike, `0` = unset, `1` = like.
672 pub rating: i8,
673}
674
675/// Request body for `POST /queue/move-to-position`.
676#[derive(Debug, Clone, Serialize)]
677#[serde(rename_all = "camelCase")]
678pub struct QueueMoveRequest {
679 /// Current 1-based index of the item to move.
680 pub start_index: u32,
681
682 /// Target 1-based index.
683 pub destination_index: u32,
684
685 /// If `true`, the response includes the updated queue.
686 #[serde(skip_serializing_if = "Option::is_none")]
687 pub return_queue: Option<bool>,
688}
689
690/// Request body for `POST /queue/remove-by-index`.
691#[derive(Debug, Clone, Serialize)]
692pub struct QueueRemoveRequest {
693 /// 1-based index of the item to remove.
694 pub index: u32,
695}
696
697/// Request body for `POST /api/v1/amapi/run-v3`.
698#[derive(Debug, Clone, Serialize)]
699pub struct AmApiRequest {
700 /// Apple Music API path (e.g. `"/v1/catalog/ca/search?term=…"`).
701 pub path: String,
702}