mirror of
https://framagit.org/veretcle/tootube.git
synced 2025-07-20 12:31:19 +02:00
558 lines
16 KiB
Rust
558 lines
16 KiB
Rust
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(¶ms)
|
||
.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(¶ms)
|
||
.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 they’re 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());
|
||
}
|
||
}
|