Files
tootube/src/peertube.rs

558 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::{config::PeertubeConfig, config::PeertubeConfigOauth2, error::TootubeError};
use log::debug;
use reqwest::{
header::{HeaderMap, HeaderValue},
multipart::Form,
Client,
};
use rpassword::prompt_password;
use serde::{Deserialize, Serialize};
use std::{boxed::Box, cmp::Ordering, error::Error, io::stdin};
#[derive(Debug, Deserialize)]
pub struct PeerTubeVideos {
pub data: Vec<PeerTubeVideo>,
}
#[derive(Debug, Deserialize)]
pub struct PeerTubeVideo {
pub name: String,
pub uuid: String,
pub description: String,
pub duration: u64,
#[serde(rename = "aspectRatio")]
pub aspect_ratio: Option<f32>,
#[serde(rename = "previewPath")]
pub preview_path: String,
#[serde(rename = "streamingPlaylists")]
pub streaming_playlists: Option<Vec<PeerTubeVideoStreamingPlaylists>>,
pub tags: Option<Vec<String>>,
pub channel: PeerTubeVideoChannel,
}
impl PeerTubeVideo {
pub fn is_short(&self) -> bool {
if self.duration < 60 && self.aspect_ratio.is_some_and(|x| x == 0.5625) {
return true;
}
false
}
}
#[derive(Debug, Deserialize)]
pub struct PeerTubeVideoChannel {
pub id: u8,
}
#[derive(Debug, Deserialize)]
pub struct PeerTubeVideoStreamingPlaylists {
pub files: Vec<PeerTubeVideoStreamingPlaylistsFiles>,
}
#[derive(Debug, Deserialize)]
struct PeerTubeOauthClientsLocalResponse {
client_id: String,
client_secret: String,
}
#[derive(Debug, Serialize)]
struct PeerTubeUsersToken {
client_id: String,
client_secret: String,
grant_type: String,
password: Option<String>,
username: Option<String>,
refresh_token: Option<String>,
}
#[derive(Debug, Deserialize)]
struct PeerTubeUsersTokenResponse {
refresh_token: String,
access_token: String,
}
#[derive(Debug, Deserialize)]
struct PeerTubeVideoSourceResponse {
#[serde(rename = "fileDownloadUrl")]
pub file_download_url: String,
}
#[derive(Debug, Deserialize)]
struct PeerTubeVideoTokenResponse {
files: PeerTubeVideoTokenResponseFiles,
}
#[derive(Debug, Deserialize)]
struct PeerTubeVideoTokenResponseFiles {
token: String,
}
#[derive(Eq, Debug, Deserialize)]
pub struct PeerTubeVideoStreamingPlaylistsFiles {
pub resolution: PeerTubeVideoStreamingPlaylistsFilesResolution,
#[serde(rename = "fileDownloadUrl")]
pub file_download_url: String,
}
impl Ord for PeerTubeVideoStreamingPlaylistsFiles {
fn cmp(&self, other: &Self) -> Ordering {
self.resolution.id.cmp(&other.resolution.id)
}
}
impl PartialOrd for PeerTubeVideoStreamingPlaylistsFiles {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for PeerTubeVideoStreamingPlaylistsFiles {
fn eq(&self, other: &Self) -> bool {
self.resolution.id == other.resolution.id
}
}
#[derive(Eq, Debug, Deserialize, PartialEq)]
pub struct PeerTubeVideoStreamingPlaylistsFilesResolution {
pub id: u16,
}
#[derive(Debug, Deserialize)]
pub struct PeerTubeVideoPlaylists {
pub total: u16,
pub data: Vec<PeerTubeVideoPlaylist>,
}
#[derive(Debug, Deserialize)]
pub struct PeerTubeVideoPlaylist {
pub uuid: String,
#[serde(rename = "displayName")]
pub display_name: String,
}
#[derive(Debug, Serialize)]
struct PeerTubeVideoPlaylistsVideos {
#[serde(rename = "videoId")]
video_id: String,
}
#[derive(Debug, Deserialize)]
struct PeerTubeVideoPlaylistResponse {
#[serde(rename = "videoPlaylist")]
video_playlist: PeerTubeVideoPlaylistResponseVideoPlaylist,
}
#[derive(Debug, Deserialize)]
struct PeerTubeVideoPlaylistResponseVideoPlaylist {
uuid: String,
}
#[derive(Debug, Deserialize)]
struct PeerTubeVideoPlaylistsPlaylistIdVideos {
total: u32,
data: Vec<PeerTubeVideoPlaylistsPlaylistIdVideosData>,
}
#[derive(Debug, Deserialize)]
struct PeerTubeVideoPlaylistsPlaylistIdVideosData {
video: PeerTubeVideo,
}
/// This function makes the registration process a little bit easier
#[tokio::main]
pub async fn register(config: &PeertubeConfig) -> Result<PeertubeConfigOauth2, Box<dyn Error>> {
// Get client ID/secret
let oauth2_client = reqwest::get(format!("{}/api/v1/oauth-clients/local", config.base_url))
.await?
.json::<PeerTubeOauthClientsLocalResponse>()
.await?;
println!(
"Please type your PeerTube username for instance {}:",
config.base_url
);
let mut username = String::new();
stdin()
.read_line(&mut username)
.expect("Unable to read back the username!");
let password = prompt_password("Your password: ").expect("Unable to read back the password!");
let params = PeerTubeUsersToken {
client_id: oauth2_client.client_id.clone(),
client_secret: oauth2_client.client_secret.clone(),
grant_type: "password".to_string(),
username: Some(username.trim().to_string()),
password: Some(password.clone()),
refresh_token: None,
};
let client = Client::new();
let oauth2_token = client
.post(format!("{}/api/v1/users/token", config.base_url))
.form(&params)
.send()
.await?
.json::<PeerTubeUsersTokenResponse>()
.await?;
println!(
"The following lines will be written to the `peertube` section of your tootube.toml file:"
);
println!("[peertube.oauth2]");
println!("client_id=\"{}\"", oauth2_client.client_id);
println!("client_secret=\"{}\"", oauth2_client.client_secret);
println!("refresh_token=\"{}\"", oauth2_token.refresh_token);
Ok(PeertubeConfigOauth2 {
client_id: oauth2_client.client_id,
client_secret: oauth2_client.client_secret,
refresh_token: oauth2_token.refresh_token,
})
}
#[derive(Debug)]
pub struct PeerTube {
base_url: String,
pub refresh_token: Option<String>,
client: Client,
}
impl PeerTube {
/// Create a new PeerTube struct with a basic embedded reqwest::Client
pub fn new(base_url: &str) -> Self {
PeerTube {
base_url: format!("{}/api/v1", base_url),
refresh_token: None,
client: Client::new(),
}
}
/// Retrieve the refresh_token and access_token and update the embedded reqwest::Client to have
/// the default required header
pub async fn with_client(
mut self,
pt_oauth2: &PeertubeConfigOauth2,
) -> Result<Self, Box<dyn Error>> {
let params = PeerTubeUsersToken {
client_id: pt_oauth2.client_id.to_owned(),
client_secret: pt_oauth2.client_secret.to_owned(),
grant_type: "refresh_token".to_string(),
refresh_token: Some(pt_oauth2.refresh_token.to_owned()),
username: None,
password: None,
};
let req = self
.client
.post(format!("{}/users/token", self.base_url))
.form(&params)
.send()
.await?
.json::<PeerTubeUsersTokenResponse>()
.await?;
let mut headers = HeaderMap::new();
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {}", req.access_token))?,
);
self.client = reqwest::Client::builder()
.default_headers(headers)
.build()?;
self.refresh_token = Some(req.refresh_token);
Ok(self)
}
/// This gets the last video uploaded to the PeerTube server
pub async fn get_latest_video(&self) -> Result<PeerTubeVideo, Box<dyn Error>> {
let body = self
.client
.get(format!(
"{}/videos?count=1&sort=-publishedAt",
self.base_url
))
.send()
.await?
.json::<PeerTubeVideos>()
.await?;
let vid = self.get_video_detail(&body.data[0].uuid).await?;
Ok(vid)
}
/// This gets all the crispy details about one particular video
pub async fn get_video_detail(&self, v: &str) -> Result<PeerTubeVideo, Box<dyn Error>> {
let body = self
.client
.get(format!("{}/videos/{}", self.base_url, v))
.send()
.await?
.json::<PeerTubeVideo>()
.await?;
Ok(body)
}
/// Get the original video source
pub async fn get_original_video_source(&self, uuid: &str) -> Result<String, Box<dyn Error>> {
let source_vid = self
.client
.get(format!("{}/videos/{}/source", self.base_url, uuid))
.send()
.await?
.json::<PeerTubeVideoSourceResponse>()
.await?;
debug!("Got the Source Vid URL: {}", &source_vid.file_download_url);
let video_file_token = self
.client
.post(format!("{}/videos/{}/token", self.base_url, uuid))
.send()
.await?
.json::<PeerTubeVideoTokenResponse>()
.await?;
debug!("Got the File Token: {}", &video_file_token.files.token);
Ok(format!(
"{}?videoFileToken={}",
source_vid.file_download_url, video_file_token.files.token
))
}
/// Delete the original video source
pub async fn delete_original_video_source(&self, uuid: &str) -> Result<(), Box<dyn Error>> {
let res = self
.client
.delete(format!("{}/videos/{}/source/file", self.base_url, uuid))
.send()
.await?;
if !res.status().is_success() {
return Err(TootubeError::new(&format!(
"Cannot delete source video file {}: {}",
uuid,
res.text().await?
))
.into());
}
Ok(())
}
/// List every playlists on PeerTube
pub async fn list_video_playlists(&self) -> Result<Vec<PeerTubeVideoPlaylist>, Box<dyn Error>> {
let mut playlists: Vec<PeerTubeVideoPlaylist> = vec![];
let mut start = 0;
let inc = 15;
while let Ok(mut local_pl) = self
.client
.get(format!(
"{}/video-playlists?count={}&playlistType=1&start={}",
self.base_url, inc, start
))
.send()
.await?
.json::<PeerTubeVideoPlaylists>()
.await
{
start += inc;
playlists.append(&mut local_pl.data);
if start >= local_pl.total {
break;
}
}
Ok(playlists)
}
/// Add a public playlist
pub async fn create_video_playlist(
&self,
c_id: u8,
display_name: &str,
) -> Result<String, Box<dyn Error>> {
let form = Form::new()
.text("displayName", display_name.to_string())
.text("privacy", "1")
.text("videoChannelId", format!("{}", c_id));
let pl_created = self
.client
.post(format!("{}/video-playlists", self.base_url))
.multipart(form)
.send()
.await?
.json::<PeerTubeVideoPlaylistResponse>()
.await?;
Ok(pl_created.video_playlist.uuid)
}
/// Add a video into a playlist
pub async fn add_video_to_playlist(
&self,
vid_uuid: &str,
pl_uuid: &str,
) -> Result<(), Box<dyn Error>> {
let video_to_add = PeerTubeVideoPlaylistsVideos {
video_id: vid_uuid.to_string(),
};
let res = self
.client
.post(format!(
"{}/video-playlists/{}/videos",
self.base_url, pl_uuid
))
.json(&video_to_add)
.send()
.await?;
if !res.status().is_success() {
return Err(TootubeError::new(&format!(
"Cannot add video {} to playlist {}: {}",
vid_uuid,
pl_uuid,
res.text().await?
))
.into());
};
Ok(())
}
/// List all videos of a playlist
pub async fn list_videos_playlist(&self, uuid: &str) -> Result<Vec<String>, Box<dyn Error>> {
let mut videos: Vec<PeerTubeVideo> = vec![];
let mut start = 0;
let inc = 15;
while let Ok(l_vid) = self
.client
.get(format!(
"{}/video-playlists/{}/videos?start={}&count={}",
&self.base_url, &uuid, start, inc
))
.send()
.await?
.json::<PeerTubeVideoPlaylistsPlaylistIdVideos>()
.await
{
start += inc;
videos.append(&mut l_vid.data.into_iter().map(|x| x.video).collect());
if start >= l_vid.total {
break;
}
}
Ok(videos.into_iter().map(|x| x.uuid).collect())
}
}
/// Given a PeerTube instance, video UUID, list of named playlists and channel ID, this function:
/// * adds playlists if they do not exists
/// * adds the video to said playlists if theyre not in it already
pub async fn get_playlists_to_be_added_to(
peertube: &PeerTube,
vid_uuid: &str,
pl: &[String],
c_id: u8,
) -> Result<Vec<String>, Box<dyn Error>> {
let mut playlist_to_be_added_to: Vec<String> = vec![];
if let Ok(local_pl) = peertube.list_video_playlists().await {
// list the displayNames of each playlist
let current_playlist: Vec<String> =
local_pl.iter().map(|s| s.display_name.clone()).collect();
// get the playlist whose displayName does not exist yet
let pl_to_create: Vec<_> = pl
.iter()
.filter(|x| !current_playlist.contains(x))
.collect();
debug!("Playlists to be added: {:?}", &pl_to_create);
playlist_to_be_added_to = local_pl
.into_iter()
.filter(|x| pl.contains(&x.display_name))
.map(|x| x.uuid.clone())
.collect();
// create the missing playlists
for p in pl_to_create {
if let Ok(s) = peertube.create_video_playlist(c_id, p).await {
playlist_to_be_added_to.push(s);
}
}
for p in playlist_to_be_added_to.clone().iter() {
if let Ok(s) = peertube.list_videos_playlist(p).await {
// if a video already exists inside a playlist, drop it
if s.contains(&vid_uuid.to_string()) {
playlist_to_be_added_to.retain(|i| *i != *p)
}
}
}
debug!("Playlists to be added to: {:?}", &playlist_to_be_added_to);
};
Ok(playlist_to_be_added_to)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_get_latest_video() {
let peertube = PeerTube::new("https://peertube.cpy.re");
let vid = peertube.get_latest_video().await.unwrap();
assert_eq!(vid.uuid.len(), 36);
}
#[tokio::test]
#[should_panic]
async fn test_get_original_video_source() {
let peertube = PeerTube::new("https://peertube.cpy.re");
let _vid = peertube
.get_original_video_source("t")
.await
.expect("Should panic!");
}
#[tokio::test]
async fn test_list_video_playlists() {
let peertube = PeerTube::new("https://peertube.cpy.re");
let pl = peertube.list_video_playlists().await.unwrap();
assert!(!pl.is_empty());
}
#[tokio::test]
async fn test_list_videos_playlist() {
let peertube = PeerTube::new("https://peertube.cpy.re");
let pl_videos = peertube
.list_videos_playlist("73a5c1fa-64c5-462d-81e5-b120781c2d72")
.await
.unwrap();
assert!(!pl_videos.is_empty());
}
}