use serde_json::Value;
use crate::error::{Error, Result};
use crate::model::{Clip, Playlist};
use super::clip::unwrap_clip;
use super::map_clip;
pub(crate) fn parse_playlists(body: &[u8]) -> Result<Vec<Playlist>> {
let data: Value = serde_json::from_slice(body)
.map_err(|err| Error::Api(format!("invalid playlist JSON: {err}")))?;
Ok(data
.get("playlists")
.and_then(Value::as_array)
.map(|raw| raw.iter().filter_map(parse_playlist_item).collect())
.unwrap_or_default())
}
fn parse_playlist_item(raw: &Value) -> Option<Playlist> {
let id = raw
.get("id")
.and_then(Value::as_str)
.filter(|id| !id.is_empty())?
.to_string();
let name = match raw.get("name") {
Some(Value::String(name)) if !name.is_empty() => name.clone(),
_ => "Untitled".to_string(),
};
let num_clips = raw
.get("num_total_results")
.and_then(Value::as_u64)
.unwrap_or(0);
Some(Playlist {
id,
name,
num_clips,
})
}
pub(crate) fn parse_playlist_clips(body: &[u8]) -> Result<(Vec<Clip>, bool)> {
let data: Value = serde_json::from_slice(body)
.map_err(|err| Error::Api(format!("invalid playlist JSON: {err}")))?;
let raw = data.get("playlist_clips").and_then(Value::as_array);
let raw_len = raw.map(|a| a.len()).unwrap_or(0);
let clips: Vec<Clip> = raw
.map(|raw| {
raw.iter()
.map(|entry| map_clip(unwrap_clip(entry)))
.filter(|clip| !clip.id.is_empty())
.collect()
})
.unwrap_or_default();
let complete = data
.get("num_total_results")
.and_then(Value::as_u64)
.is_some_and(|total| raw_len as u64 == total && clips.len() == raw_len);
Ok((clips, complete))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_playlists_reads_fields_and_drops_idless_defaulting_name() {
let body = serde_json::json!({
"playlists": [
{"id": "p1", "name": "Mix", "num_total_results": 3},
{"id": "", "name": "Ghost"},
{"name": "No Id"},
{"id": "p2"}
]
})
.to_string();
let playlists = parse_playlists(body.as_bytes()).unwrap();
assert_eq!(playlists.len(), 2);
assert_eq!(playlists[0].id, "p1");
assert_eq!(playlists[0].name, "Mix");
assert_eq!(playlists[0].num_clips, 3);
assert_eq!(playlists[1].id, "p2");
assert_eq!(playlists[1].name, "Untitled");
assert_eq!(playlists[1].num_clips, 0);
}
#[test]
fn parse_playlists_rejects_non_json_bytes() {
assert!(parse_playlists(b"nope").is_err());
}
#[test]
fn parse_playlist_clips_unwraps_members_and_preserves_order() {
let body = serde_json::json!({
"num_total_results": 2,
"playlist_clips": [
{"clip": {"id": "a", "title": "A"}},
{"id": "b", "title": "B"}
]
})
.to_string();
let (clips, complete) = parse_playlist_clips(body.as_bytes()).unwrap();
assert_eq!(
clips.iter().map(|c| c.id.as_str()).collect::<Vec<_>>(),
["a", "b"]
);
assert!(complete);
}
#[test]
fn parse_playlist_clips_completeness_gates_deletion_authority() {
let short = serde_json::json!({
"num_total_results": 5,
"playlist_clips": [{"id": "a"}, {"id": "b"}]
})
.to_string();
assert!(!parse_playlist_clips(short.as_bytes()).unwrap().1);
let missing_total = serde_json::json!({"playlist_clips": [{"id": "a"}]}).to_string();
assert!(!parse_playlist_clips(missing_total.as_bytes()).unwrap().1);
let dropped = serde_json::json!({
"num_total_results": 2,
"playlist_clips": [{"id": "a"}, {"id": ""}]
})
.to_string();
let (clips, complete) = parse_playlist_clips(dropped.as_bytes()).unwrap();
assert_eq!(clips.len(), 1);
assert!(!complete);
}
#[test]
fn parse_playlist_clips_rejects_non_json_bytes() {
assert!(parse_playlist_clips(b"nope").is_err());
}
}