use serde_json::{Value, json};
use super::clients::{ORIGIN_YOUTUBE_MUSIC, WEB_REMIX};
use super::innertube::sapisid_hash;
async fn post(endpoint: &str, body: Value, cookies: &str) -> Result<Value, String> {
let client = WEB_REMIX;
let auth =
sapisid_hash(cookies, ORIGIN_YOUTUBE_MUSIC).ok_or_else(|| "SAPISID missing".to_string())?;
let resp = super::innertube::http_client()
.clone()
.post(format!(
"{ORIGIN_YOUTUBE_MUSIC}/youtubei/v1/{endpoint}?prettyPrint=false"
))
.header("User-Agent", client.user_agent)
.header("Content-Type", "application/json")
.header("X-Goog-Api-Format-Version", "1")
.header("X-YouTube-Client-Name", client.client_id)
.header("X-YouTube-Client-Version", client.client_version)
.header("X-Origin", ORIGIN_YOUTUBE_MUSIC)
.header("Referer", format!("{ORIGIN_YOUTUBE_MUSIC}/"))
.header("Cookie", cookies)
.header("Authorization", auth)
.json(&body)
.send()
.await
.map_err(|e| format!("{endpoint} HTTP: {e}"))?;
if !resp.status().is_success() {
return Err(format!("{endpoint} HTTP {}", resp.status()));
}
resp.json::<Value>()
.await
.map_err(|e| format!("{endpoint} JSON parse: {e}"))
}
fn ytmusic_context() -> Value {
json!({
"client": {
"clientName": WEB_REMIX.client_name,
"clientVersion": WEB_REMIX.client_version,
"hl": "en",
"gl": "US",
},
})
}
#[tracing::instrument(name = "yt.like", skip(cookies), fields(video_id = %video_id))]
pub async fn like_video(video_id: &str, cookies: &str) -> Result<(), String> {
let body = json!({
"context": { "client": ytmusic_context()["client"], "user": { "lockedSafetyMode": false } },
"target": { "videoId": video_id },
});
post("like/like", body, cookies).await.map(|_| ())
}
#[tracing::instrument(name = "yt.unlike", skip(cookies), fields(video_id = %video_id))]
pub async fn unlike_video(video_id: &str, cookies: &str) -> Result<(), String> {
let body = json!({
"context": { "client": ytmusic_context()["client"], "user": { "lockedSafetyMode": false } },
"target": { "videoId": video_id },
});
post("like/removelike", body, cookies).await.map(|_| ())
}
#[tracing::instrument(name = "yt.playlist_add", skip(cookies), fields(playlist_id = %playlist_id, video_id = %video_id))]
pub async fn add_to_playlist(
playlist_id: &str,
video_id: &str,
cookies: &str,
) -> Result<(), String> {
let body = json!({
"context": { "client": ytmusic_context()["client"], "user": { "lockedSafetyMode": false } },
"playlistId": playlist_id,
"actions": [{
"action": "ACTION_ADD_VIDEO",
"addedVideoId": video_id,
}],
});
post("browse/edit_playlist", body, cookies)
.await
.map(|_| ())
}
#[tracing::instrument(name = "yt.playlist_remove", skip(cookies), fields(playlist_id = %playlist_id, video_id = %video_id))]
pub async fn remove_from_playlist(
playlist_id: &str,
video_id: &str,
cookies: &str,
) -> Result<(), String> {
let body = json!({
"context": { "client": ytmusic_context()["client"], "user": { "lockedSafetyMode": false } },
"playlistId": playlist_id,
"actions": [{
"action": "ACTION_REMOVE_VIDEO_BY_VIDEO_ID",
"removedVideoId": video_id,
}],
});
post("browse/edit_playlist", body, cookies)
.await
.map(|_| ())
}
#[tracing::instrument(name = "yt.playlist_create", skip(cookies, video_ids), fields(title = %title, count = video_ids.len()))]
pub async fn create_playlist(
title: &str,
video_ids: &[&str],
cookies: &str,
) -> Result<String, String> {
let body = json!({
"context": { "client": ytmusic_context()["client"], "user": { "lockedSafetyMode": false } },
"title": title,
"description": "",
"privacyStatus": "PRIVATE",
"videoIds": video_ids,
});
let resp = post("playlist/create", body, cookies).await?;
resp.get("playlistId")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| "create_playlist: no playlistId in response".to_string())
}