161 lines
4.3 KiB
Rust
Executable File
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,
|
|
}
|
|
}
|