Change derives + Reorganize routes + Improve channel ID checking
This commit is contained in:
parent
4b8cccf29f
commit
a210d7003b
@ -13,7 +13,7 @@ You can get a binary build in the [Releases](https://git.roaming97.com/Almond/ap
|
|||||||
|
|
||||||
However, if no release build is compatible with your system, you can build one yourself by following the [Building](#building) guide.
|
However, if no release build is compatible with your system, you can build one yourself by following the [Building](#building) guide.
|
||||||
|
|
||||||
To learn how to use the Almond API, you can read the documentation [here](todo) (TODO: Generate OpenAPI docs).
|
To learn how to use the Almond API, you can read the documentation [here](todo) (TODO: Generate stable OpenAPI docs).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ To learn how to use the Almond API, you can read the documentation [here](todo)
|
|||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Since this project is written in Rust, you will need [Rust](https://rust-lang.org/) installed on your system. The site has an [installation](https://www.rust-lang.org/tools/install) page that includes instructions for macOS, Linux, Windows, and more.
|
Since this project is written in Rust, you will need [Rust](https://rust-lang.org/) installed on your system, check these [installation options](https://rustup.rs/)
|
||||||
|
|
||||||
If you have Git installed on your system and are comfortable using a terminal, you can clone the repository with the following command:
|
If you have Git installed on your system and are comfortable using a terminal, you can clone the repository with the following command:
|
||||||
|
|
||||||
@ -42,6 +42,6 @@ If you have Git installed on your system and are comfortable using a terminal, y
|
|||||||
$ git clone https://git.roaming97.com/Almond/api.git
|
$ git clone https://git.roaming97.com/Almond/api.git
|
||||||
```
|
```
|
||||||
|
|
||||||
If not, you can download a file by clicking on the "Code" button at the top and extract the source code.
|
If not, you can download a file by clicking on the "Code" button at the top. Once downloaded, extract the source code.
|
||||||
|
|
||||||
TODO: Finish build guide
|
TODO: Finish build guide
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use std::{io, path::Path};
|
use std::{io, path::Path, str::Utf8Error};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::fs;
|
use tokio::{fs, process::Command};
|
||||||
use tracing::{info, warn};
|
use tracing::{error, info, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::string::ToUnquotedString;
|
use crate::string::ToUnquotedString;
|
||||||
@ -15,15 +15,21 @@ pub enum ChannelError {
|
|||||||
InvalidUrl,
|
InvalidUrl,
|
||||||
#[error("Channel already exists in database")]
|
#[error("Channel already exists in database")]
|
||||||
AlreadyExists,
|
AlreadyExists,
|
||||||
#[error("IO Error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
IOError(#[from] io::Error),
|
IOError(#[from] io::Error),
|
||||||
|
#[error("UTF-8 error: {0}")]
|
||||||
|
Utf8Error(#[from] Utf8Error),
|
||||||
|
#[error("Invalid output from ID extraction function")]
|
||||||
|
InvalidOutput,
|
||||||
|
#[error("Failed to extract channel ID from '{0}'")]
|
||||||
|
ExtractID(String),
|
||||||
#[error("Could not serialize info JSON: {0}")]
|
#[error("Could not serialize info JSON: {0}")]
|
||||||
SerializeInfoJSON(#[from] serde_json::Error),
|
SerializeInfoJSON(#[from] serde_json::Error),
|
||||||
#[error("Failed to parse value from key '{0}'")]
|
#[error("Failed to parse value from key '{0}'")]
|
||||||
JsonKey(String),
|
JsonKey(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
@ -38,10 +44,12 @@ pub struct Channel {
|
|||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
async fn yt_dlp_task(url: &str) -> Result<(), ChannelError> {
|
async fn yt_dlp_task(url: &str) -> Result<(), ChannelError> {
|
||||||
let mut child = tokio::process::Command::new("yt-dlp")
|
let mut child = Command::new("yt-dlp")
|
||||||
.args([
|
.args([
|
||||||
"--write-info-json",
|
"--write-info-json",
|
||||||
"--skip-download",
|
"--skip-download",
|
||||||
|
"--playlist-items",
|
||||||
|
"1",
|
||||||
"-v",
|
"-v",
|
||||||
"-o",
|
"-o",
|
||||||
"media/channels/tmp/tmp.%(ext)s",
|
"media/channels/tmp/tmp.%(ext)s",
|
||||||
@ -55,6 +63,40 @@ impl Channel {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Transforms a channel handle or username into a canonical channel ID.
|
||||||
|
///
|
||||||
|
/// While it invariably returns a canonical ID even if the URL already contains one,
|
||||||
|
/// it's not recommended to call this function when that is the case in order to
|
||||||
|
/// prevent multiple calls to yt-dlp.
|
||||||
|
async fn extract_id(url: &str) -> Result<String, ChannelError> {
|
||||||
|
let output = Command::new("yt-dlp")
|
||||||
|
.args([
|
||||||
|
"--skip-download",
|
||||||
|
"--playlist-items",
|
||||||
|
"1",
|
||||||
|
"--print",
|
||||||
|
"channel_id",
|
||||||
|
url,
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
error!(
|
||||||
|
"yt-dlp could not download channel. Does the URL point to a valid/existing channel?"
|
||||||
|
);
|
||||||
|
return Err(ChannelError::InvalidOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = str::from_utf8(&output.stdout)?;
|
||||||
|
let mut lines = stdout.lines().filter(|line| line.starts_with("UC"));
|
||||||
|
|
||||||
|
lines.next().map_or_else(
|
||||||
|
|| Err(ChannelError::ExtractID(url.into())),
|
||||||
|
|id| Ok(id.into()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn from_url(url: &Url, id: i64) -> Result<Self, ChannelError> {
|
pub async fn from_url(url: &Url, id: i64) -> Result<Self, ChannelError> {
|
||||||
let url_path = url.path();
|
let url_path = url.path();
|
||||||
|
|
||||||
@ -65,7 +107,30 @@ impl Channel {
|
|||||||
return Err(ChannelError::InvalidUrl);
|
return Err(ChannelError::InvalidUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::yt_dlp_task(url.as_str()).await?;
|
let url = url.as_str();
|
||||||
|
let youtube_id = if let Some(id) = url_path.strip_prefix("/channel/") {
|
||||||
|
if id.starts_with("UC") {
|
||||||
|
id.into()
|
||||||
|
} else {
|
||||||
|
warn!("Channel ID contains unexpected format: {id}, extracting");
|
||||||
|
Self::extract_id(url).await?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("URL is not canonical, extracting ID");
|
||||||
|
Self::extract_id(url).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
let dir = format!("media/channels/{youtube_id}");
|
||||||
|
let file_stem = format!("{dir}/{youtube_id}");
|
||||||
|
|
||||||
|
if Path::new(&dir).exists() {
|
||||||
|
warn!("Channel already exists, skipping");
|
||||||
|
return Err(ChannelError::AlreadyExists);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::create_dir(dir).await?;
|
||||||
|
|
||||||
|
Self::yt_dlp_task(url).await?;
|
||||||
let info: Value =
|
let info: Value =
|
||||||
serde_json::from_str(&fs::read_to_string("media/channels/tmp/tmp.info.json").await?)?;
|
serde_json::from_str(&fs::read_to_string("media/channels/tmp/tmp.info.json").await?)?;
|
||||||
|
|
||||||
@ -75,18 +140,6 @@ impl Channel {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
let youtube_id = get_info_value("channel_id").to_unquoted_string();
|
|
||||||
let dir = format!("media/channels/{youtube_id}");
|
|
||||||
let file_stem = format!("{dir}/{youtube_id}");
|
|
||||||
|
|
||||||
// TODO: Detect if ID exists without running yt-dlp task
|
|
||||||
if Path::new(&dir).exists() {
|
|
||||||
warn!("Channel already exists, skipping");
|
|
||||||
return Err(ChannelError::AlreadyExists);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::create_dir(dir).await?;
|
|
||||||
|
|
||||||
let url = get_info_value("channel_url").to_unquoted_string();
|
let url = get_info_value("channel_url").to_unquoted_string();
|
||||||
let name = get_info_value("channel").to_unquoted_string();
|
let name = get_info_value("channel").to_unquoted_string();
|
||||||
let handle_url = get_info_value("uploader_url").to_unquoted_string();
|
let handle_url = get_info_value("uploader_url").to_unquoted_string();
|
||||||
|
@ -19,7 +19,7 @@ pub enum CommentsError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct Comment {
|
pub struct Comment {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub video_id: String,
|
pub video_id: String,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
pub struct Address {
|
pub struct Address {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
@ -21,7 +21,7 @@ impl Address {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
pub struct Pagination {
|
pub struct Pagination {
|
||||||
pub videos: usize,
|
pub videos: usize,
|
||||||
pub comments: usize,
|
pub comments: usize,
|
||||||
@ -38,7 +38,7 @@ impl Default for Pagination {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Deserialize)]
|
#[derive(Default, Clone, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub address: Address,
|
pub address: Address,
|
||||||
pub pagination: Pagination,
|
pub pagination: Pagination,
|
||||||
|
@ -7,7 +7,7 @@ use tracing::{error, warn};
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
/// This controls an instance and its state.
|
/// This controls an instance and its state.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Instance {
|
pub struct Instance {
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
pub key_hash: String,
|
pub key_hash: String,
|
||||||
|
32
src/main.rs
32
src/main.rs
@ -2,15 +2,15 @@ use std::net::SocketAddr;
|
|||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
routing::{get, post},
|
routing::{delete, get, post, put},
|
||||||
};
|
};
|
||||||
use instance::Instance;
|
use instance::Instance;
|
||||||
use routes::{
|
use routes::{
|
||||||
channel::{get_channel, list_channels, upload_channel},
|
channel::{delete_channel, get_channel, list_channels, update_channel, upload_channel},
|
||||||
comment::get_comments,
|
comment::get_video_comments,
|
||||||
index,
|
index,
|
||||||
middleware::auth,
|
middleware::auth,
|
||||||
video::{get_video, list_videos, upload_video},
|
video::{delete_video, get_channel_videos, get_video, list_videos, update_video, upload_video},
|
||||||
};
|
};
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@ -42,17 +42,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let address = instance.config.address.get_url();
|
let address = instance.config.address.get_url();
|
||||||
|
|
||||||
let almond = Router::new()
|
let router = Router::new()
|
||||||
.route("/upload_video", post(upload_video))
|
|
||||||
.route("/upload_channel", post(upload_channel))
|
|
||||||
.route_layer(axum::middleware::from_fn_with_state(instance.clone(), auth))
|
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.route("/videos", get(list_videos))
|
.route("/video", get(list_videos))
|
||||||
.route("/video/{id}", get(get_video))
|
.route("/video/{id}", get(get_video))
|
||||||
.route("/comments/{id}", get(get_comments))
|
.route("/video/{id}/comments", get(get_video_comments))
|
||||||
.route("/channels", get(list_channels))
|
.route("/channel", get(list_channels))
|
||||||
.route("/channel/{id}", get(get_channel))
|
.route("/channel/{id}", get(get_channel))
|
||||||
.with_state(instance);
|
.route("/channel/{id}/videos", get(get_channel_videos));
|
||||||
|
|
||||||
|
let protected = Router::new()
|
||||||
|
.route("/video", post(upload_video))
|
||||||
|
.route("/video/{id}", put(update_video))
|
||||||
|
.route("/video/{id}", delete(delete_video))
|
||||||
|
.route("/channel", post(upload_channel))
|
||||||
|
.route("/channel/{id}", put(update_channel))
|
||||||
|
.route("/channel/{id}", delete(delete_channel))
|
||||||
|
.route_layer(axum::middleware::from_fn_with_state(instance.clone(), auth));
|
||||||
|
|
||||||
|
let almond = router.merge(protected).with_state(instance);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(address).await?;
|
let listener = tokio::net::TcpListener::bind(address).await?;
|
||||||
|
|
||||||
|
@ -13,12 +13,12 @@ use crate::{
|
|||||||
url::is_youtube_url,
|
url::is_youtube_url,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ListChannelsQuery {
|
pub struct ListChannelsQuery {
|
||||||
page: Option<usize>,
|
page: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct ListChannelsResponse {
|
pub struct ListChannelsResponse {
|
||||||
channels: Vec<Channel>,
|
channels: Vec<Channel>,
|
||||||
page: usize,
|
page: usize,
|
||||||
@ -76,7 +76,7 @@ pub async fn get_channel(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct UploadChannelQuery {
|
pub struct UploadChannelQuery {
|
||||||
url: String,
|
url: String,
|
||||||
}
|
}
|
||||||
@ -109,7 +109,11 @@ pub async fn upload_channel(
|
|||||||
|
|
||||||
let new_channel = Channel::from_url(&url, id).await.map_err(|e| match e {
|
let new_channel = Channel::from_url(&url, id).await.map_err(|e| match e {
|
||||||
ChannelError::AlreadyExists => StatusCode::OK,
|
ChannelError::AlreadyExists => StatusCode::OK,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
ChannelError::InvalidOutput => StatusCode::BAD_REQUEST,
|
||||||
|
e => {
|
||||||
|
error!("{e:?}");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
match new_channel {
|
match new_channel {
|
||||||
@ -148,3 +152,36 @@ pub async fn upload_channel(
|
|||||||
Err(status) => status,
|
Err(status) => status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update an existing channel from the database
|
||||||
|
pub async fn update_channel(State(state): State<Instance>, Path(id): Path<String>) -> StatusCode {
|
||||||
|
match sqlx::query_as!(Channel, "SELECT * FROM channel WHERE youtube_id = ?", id)
|
||||||
|
.fetch_optional(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_or(Err(StatusCode::INTERNAL_SERVER_ERROR), |channel| {
|
||||||
|
channel.ok_or(StatusCode::NOT_FOUND)
|
||||||
|
}) {
|
||||||
|
// TODO
|
||||||
|
Ok(_channel) => StatusCode::OK,
|
||||||
|
Err(status) => {
|
||||||
|
error!("Failed to update video '{id}'");
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a channel from the database
|
||||||
|
pub async fn delete_channel(State(state): State<Instance>, Path(id): Path<String>) -> StatusCode {
|
||||||
|
match sqlx::query!("DELETE FROM channel WHERE youtube_id = ?", id)
|
||||||
|
.fetch_optional(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_or(Err(StatusCode::INTERNAL_SERVER_ERROR), |channel| {
|
||||||
|
channel.ok_or(StatusCode::NOT_FOUND)
|
||||||
|
}) {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Deleted channel '{id}' successfully");
|
||||||
|
StatusCode::OK
|
||||||
|
}
|
||||||
|
Err(status) => status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -11,12 +11,12 @@ use crate::{
|
|||||||
instance::Instance,
|
instance::Instance,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct VideoCommentsQuery {
|
pub struct VideoCommentsQuery {
|
||||||
page: Option<usize>,
|
page: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct VideoCommentsResponse {
|
pub struct VideoCommentsResponse {
|
||||||
comments: Vec<Comment>,
|
comments: Vec<Comment>,
|
||||||
page: usize,
|
page: usize,
|
||||||
@ -26,7 +26,7 @@ pub struct VideoCommentsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches the comments from a video, will return an empty vec if the video has no comments
|
/// Fetches the comments from a video, will return an empty vec if the video has no comments
|
||||||
pub async fn get_comments(
|
pub async fn get_video_comments(
|
||||||
State(state): State<Instance>,
|
State(state): State<Instance>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Query(query): Query<VideoCommentsQuery>,
|
Query(query): Query<VideoCommentsQuery>,
|
||||||
|
@ -12,7 +12,7 @@ use tracing::error;
|
|||||||
|
|
||||||
use crate::instance::Instance;
|
use crate::instance::Instance;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
#[derive(PartialEq, Eq, Deserialize)]
|
||||||
pub struct Key(pub String);
|
pub struct Key(pub String);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -6,7 +6,7 @@ pub mod comment;
|
|||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
pub mod video;
|
pub mod video;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct IndexResponse {
|
pub struct IndexResponse {
|
||||||
app_name: String,
|
app_name: String,
|
||||||
version: String,
|
version: String,
|
||||||
|
@ -13,12 +13,12 @@ use crate::{
|
|||||||
video::{Video, VideoError},
|
video::{Video, VideoError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ListVideosQuery {
|
pub struct ListVideosQuery {
|
||||||
page: Option<usize>,
|
page: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct ListVideosResponse {
|
pub struct ListVideosResponse {
|
||||||
videos: Vec<Video>,
|
videos: Vec<Video>,
|
||||||
page: usize,
|
page: usize,
|
||||||
@ -63,6 +63,23 @@ pub async fn list_videos(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all videos belonging to a channel ID from the database
|
||||||
|
pub async fn get_channel_videos(
|
||||||
|
State(state): State<Instance>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<Json<Vec<Video>>, StatusCode> {
|
||||||
|
sqlx::query_as!(Video, "SELECT * FROM video WHERE author_id = ?", id)
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_or(Err(StatusCode::INTERNAL_SERVER_ERROR), |videos| {
|
||||||
|
if videos.is_empty() {
|
||||||
|
Err(StatusCode::NOT_FOUND)
|
||||||
|
} else {
|
||||||
|
Ok(Json(videos))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a single video from the database by its ID
|
/// Get a single video from the database by its ID
|
||||||
pub async fn get_video(
|
pub async fn get_video(
|
||||||
State(state): State<Instance>,
|
State(state): State<Instance>,
|
||||||
@ -76,7 +93,7 @@ pub async fn get_video(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct UploadVideoQuery {
|
pub struct UploadVideoQuery {
|
||||||
url: String,
|
url: String,
|
||||||
}
|
}
|
||||||
@ -107,13 +124,11 @@ pub async fn upload_video(
|
|||||||
return StatusCode::BAD_REQUEST;
|
return StatusCode::BAD_REQUEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_video = Video::from_url(&url, id)
|
let new_video = Video::from_url(&url, id).await.map_err(|e| match e {
|
||||||
.await
|
VideoError::InvalidUrl => StatusCode::BAD_REQUEST,
|
||||||
.map_err(|e| match e {
|
VideoError::AlreadyExists => StatusCode::OK,
|
||||||
VideoError::InvalidUrl => StatusCode::BAD_REQUEST,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
VideoError::AlreadyExists => StatusCode::OK,
|
});
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
});
|
|
||||||
|
|
||||||
match new_video {
|
match new_video {
|
||||||
Ok(video) => {
|
Ok(video) => {
|
||||||
@ -158,3 +173,36 @@ pub async fn upload_video(
|
|||||||
Err(status) => status,
|
Err(status) => status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update an existing video from the database
|
||||||
|
pub async fn update_video(State(state): State<Instance>, Path(id): Path<String>) -> StatusCode {
|
||||||
|
match 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.ok_or(StatusCode::NOT_FOUND)
|
||||||
|
}) {
|
||||||
|
// TODO
|
||||||
|
Ok(_video) => StatusCode::OK,
|
||||||
|
Err(status) => {
|
||||||
|
error!("Failed to update video '{id}'");
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a video from the database
|
||||||
|
pub async fn delete_video(State(state): State<Instance>, Path(id): Path<String>) -> StatusCode {
|
||||||
|
match sqlx::query!("DELETE FROM video WHERE youtube_id = ?", id)
|
||||||
|
.fetch_optional(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_or(Err(StatusCode::INTERNAL_SERVER_ERROR), |video| {
|
||||||
|
video.ok_or(StatusCode::NOT_FOUND)
|
||||||
|
}) {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Deleted video '{id}' successfully");
|
||||||
|
StatusCode::OK
|
||||||
|
}
|
||||||
|
Err(status) => status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -26,7 +26,7 @@ pub enum VideoError {
|
|||||||
MissingVideoFile,
|
MissingVideoFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct Video {
|
pub struct Video {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user