api/src/routes/video.rs

161 lines
4.3 KiB
Rust
Executable File

use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
};
use serde::{Deserialize, Serialize};
use tracing::{error, info};
use url::Url;
use crate::{
instance::Instance,
url::is_youtube_url,
video::{Video, VideoError},
};
#[derive(Debug, Deserialize)]
pub struct ListVideosQuery {
page: Option<usize>,
}
#[derive(Debug, Serialize)]
pub struct ListVideosResponse {
videos: Vec<Video>,
page: usize,
per_page: usize,
total: usize,
pages: usize,
}
/// Retrieve video list as JSON (paged)
pub async fn list_videos(
State(state): State<Instance>,
Query(query): Query<ListVideosQuery>,
) -> Result<Json<ListVideosResponse>, StatusCode> {
let Ok(videos) = sqlx::query_as!(Video, "SELECT * FROM video")
.fetch_all(&state.pool)
.await
else {
error!("Could not fetch videos from database!");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
};
let per_page = state.config.pagination.videos;
let total = videos.len();
let pages = total.div_ceil(per_page);
let page = query.page.unwrap_or(1).min(pages).max(1);
let start = per_page * (page - 1);
let end = (start + per_page).min(total);
let videos = if start < total {
videos[start..end].to_vec()
} else {
vec![]
};
Ok(Json(ListVideosResponse {
videos,
page,
per_page,
total,
pages,
}))
}
/// Get a single video from the database by its ID
pub async fn get_video(
State(state): State<Instance>,
Path(id): Path<String>,
) -> Result<Json<Video>, StatusCode> {
sqlx::query_as!(Video, "SELECT * FROM video WHERE youtube_id = ?", id)
.fetch_optional(&state.pool)
.await
.map_or(Err(StatusCode::INTERNAL_SERVER_ERROR), |video| {
video.map_or(Err(StatusCode::NOT_FOUND), |v| Ok(Json(v)))
})
}
#[derive(Debug, Deserialize)]
pub struct UploadVideoQuery {
url: String,
}
/// Upload a video to the database
pub async fn upload_video(
State(state): State<Instance>,
Query(query): Query<UploadVideoQuery>,
) -> StatusCode {
let id = match sqlx::query_scalar!("SELECT MAX(id) FROM video")
.fetch_one(&state.pool)
.await
{
Ok(Some(max_id)) => max_id + 1,
Ok(None) => 0,
Err(_) => {
return StatusCode::INTERNAL_SERVER_ERROR;
}
};
let Ok(url) = Url::parse(&query.url) else {
error!("Could not parse URL!");
return StatusCode::BAD_REQUEST;
};
if !is_youtube_url(&url) {
error!("YouTube URL RegEx match failed!");
return StatusCode::BAD_REQUEST;
}
let new_video = Video::from_url(&url, id)
.await
.map_err(|e| match e {
VideoError::InvalidUrl => StatusCode::BAD_REQUEST,
VideoError::AlreadyExists => StatusCode::OK,
_ => StatusCode::INTERNAL_SERVER_ERROR,
});
match new_video {
Ok(video) => {
match sqlx::query!(
"
INSERT INTO video (
id, url, youtube_id, title, description, author, author_id, author_url,
views, upload_date, likes, dislikes, file_name, file_size, sha256, thumbnail
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
",
video.id,
video.url,
video.youtube_id,
video.title,
video.description,
video.author,
video.author_id,
video.author_url,
video.views,
video.upload_date,
video.likes,
video.dislikes,
video.file_name,
video.file_size,
video.sha256,
video.thumbnail,
)
.execute(&state.pool)
.await
{
Ok(result) => {
info!("Inserted video to database successfully! {result:?}");
StatusCode::OK
}
Err(e) => {
error!("Error inserting video to database: {e:?}");
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
Err(status) => status,
}
}