Comments support
This commit is contained in:
parent
628f179e09
commit
12dae0dd5e
34
Cargo.lock
generated
34
Cargo.lock
generated
@ -192,9 +192,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.18"
|
||||
version = "1.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
|
||||
checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@ -1414,9 +1414,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
|
||||
checksum = "14e22987355fbf8cfb813a0cf8cd97b1b4ec834b94dbd759a9e8679d41fabe83"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
@ -1427,10 +1427,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
|
||||
checksum = "55c4720d7d4cd3d5b00f61d03751c685ad09c33ae8290c8a2c11335e0604300b"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"crc",
|
||||
"crossbeam-queue",
|
||||
@ -1460,9 +1461,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
|
||||
checksum = "175147fcb75f353ac7675509bc58abb2cb291caf0fd24a3623b8f7e3eb0a754b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1473,9 +1474,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros-core"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
|
||||
checksum = "1cde983058e53bfa75998e1982086c5efe3c370f3250bf0357e344fa3352e32b"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
@ -1499,9 +1500,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-mysql"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
|
||||
checksum = "847d2e5393a4f39e47e4f36cab419709bc2b83cbe4223c60e86e1471655be333"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
@ -1541,9 +1542,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
|
||||
checksum = "cc35947a541b9e0a2e3d85da444f1c4137c13040267141b208395a0d0ca4659f"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
@ -1578,9 +1579,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-sqlite"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
|
||||
checksum = "6c48291dac4e5ed32da0927a0b981788be65674aeb62666d19873ab4289febde"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"flume",
|
||||
@ -1595,6 +1596,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_urlencoded",
|
||||
"sqlx-core",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
@ -1,3 +1,4 @@
|
||||
host = "127.0.0.1"
|
||||
port = 3000
|
||||
videos_per_page = 10
|
||||
comments_per_page = 10
|
||||
|
@ -1,2 +1,4 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct Channel;
|
||||
|
@ -1,25 +1,77 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub enum CommentFlags {
|
||||
Uploader,
|
||||
Verified,
|
||||
Hearted,
|
||||
Pinned,
|
||||
#[default]
|
||||
None,
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
use tokio::fs;
|
||||
use tracing::{error, warn};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CommentsError {
|
||||
#[error("Target video {0} not found in database")]
|
||||
InvalidTarget(String),
|
||||
#[error("IO Error: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
#[error("Could not serialize info JSON: {0}")]
|
||||
SerializeInfoJSON(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Comment {
|
||||
pub id: String,
|
||||
pub video_id: String,
|
||||
pub parent: String,
|
||||
pub text: String,
|
||||
pub like_count: i64,
|
||||
pub author_id: String,
|
||||
pub author: String,
|
||||
pub author_thumbnail: Option<String>,
|
||||
pub author_url: Option<String>,
|
||||
pub author_thumbnail: String,
|
||||
pub author_url: String,
|
||||
pub timestamp: i64,
|
||||
pub flags: CommentFlags,
|
||||
pub author_is_uploader: bool,
|
||||
pub author_is_verified: bool,
|
||||
pub is_hearted: bool,
|
||||
pub is_pinned: bool,
|
||||
}
|
||||
|
||||
pub async fn get_comments_from_video(id: &str) -> Result<Vec<Comment>, CommentsError> {
|
||||
let info_json = format!("videos/{id}/{id}.info.json");
|
||||
let info_json = Path::new(&info_json);
|
||||
|
||||
if !info_json.exists() {
|
||||
return Err(CommentsError::InvalidTarget(id.into()));
|
||||
}
|
||||
|
||||
let info: Value = serde_json::from_str(&fs::read_to_string(info_json).await?)?;
|
||||
|
||||
let Some(comments) = info.get("comments") else {
|
||||
warn!("Video {id} has no comments!");
|
||||
return Ok(vec![]);
|
||||
};
|
||||
|
||||
comments.as_array().map_or_else(
|
||||
|| Ok(vec![]),
|
||||
|comments| {
|
||||
Ok(comments
|
||||
.iter()
|
||||
.map(|c| Comment {
|
||||
id: c["id"].to_string(),
|
||||
video_id: id.into(),
|
||||
parent: c["parent"].to_string(),
|
||||
text: c["text"].to_string(),
|
||||
like_count: c["like_count"].as_i64().unwrap_or_default(),
|
||||
author_id: c["author_id"].to_string(),
|
||||
author: c["author"].to_string(),
|
||||
author_thumbnail: c["author_thumbnail"].to_string(),
|
||||
author_url: c["author_url"].to_string(),
|
||||
timestamp: c["timestamp"].as_i64().unwrap_or_default(),
|
||||
author_is_uploader: c["author_is_uploader"].as_bool().unwrap_or_default(),
|
||||
author_is_verified: c["author_is_verified"].as_bool().unwrap_or_default(),
|
||||
is_hearted: c["is_favorited"].as_bool().unwrap_or_default(),
|
||||
is_pinned: c["is_pinned"].as_bool().unwrap_or_default(),
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ pub struct Config {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub videos_per_page: usize,
|
||||
pub comments_per_page: usize,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@ -18,6 +19,7 @@ impl Default for Config {
|
||||
host: "0.0.0.0".into(),
|
||||
port: 3000,
|
||||
videos_per_page: 10,
|
||||
comments_per_page: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,14 @@ use axum::{
|
||||
};
|
||||
use instance::Instance;
|
||||
use middleware::auth;
|
||||
use routes::{get_video, list_videos, upload_video};
|
||||
use routes::{
|
||||
comment::video_comments,
|
||||
video::{get_video, list_videos, upload_video},
|
||||
};
|
||||
use tokio::signal;
|
||||
use tracing::info;
|
||||
|
||||
mod comment;
|
||||
mod instance;
|
||||
mod middleware;
|
||||
mod routes;
|
||||
@ -31,7 +35,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.route("/upload", post(upload_video))
|
||||
.route_layer(axum::middleware::from_fn_with_state(instance.clone(), auth))
|
||||
.route("/", get(list_videos))
|
||||
.route("/{id}", get(get_video))
|
||||
.route("/video/{id}", get(get_video))
|
||||
.route("/comments/{id}", get(video_comments))
|
||||
.with_state(instance);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(address).await?;
|
||||
|
68
src/routes/comment.rs
Executable file
68
src/routes/comment.rs
Executable file
@ -0,0 +1,68 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
comment::{Comment, CommentsError, get_comments_from_video},
|
||||
instance::Instance,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VideoCommentsQuery {
|
||||
page: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct VideoCommentsResponse {
|
||||
comments: Vec<Comment>,
|
||||
page: usize,
|
||||
per_page: usize,
|
||||
total: usize,
|
||||
pages: usize,
|
||||
}
|
||||
|
||||
/// Fetches the comments from a video, will return an empty vec if the video has no comments
|
||||
pub async fn video_comments(
|
||||
State(state): State<Instance>,
|
||||
Path(id): Path<String>,
|
||||
Query(query): Query<VideoCommentsQuery>,
|
||||
) -> Result<Json<VideoCommentsResponse>, StatusCode> {
|
||||
let comments = get_comments_from_video(&id).await.map_err(|e| match e {
|
||||
CommentsError::InvalidTarget(t) => {
|
||||
error!("Video {t} does not exist in database!");
|
||||
StatusCode::NOT_FOUND
|
||||
}
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
|
||||
match comments {
|
||||
Ok(comments) => {
|
||||
let per_page = state.config.comments_per_page;
|
||||
let total = comments.len();
|
||||
let pages = total.div_ceil(per_page);
|
||||
let page = query.page.unwrap_or(1).max(1).min(pages);
|
||||
|
||||
let start = per_page * (page - 1);
|
||||
let end = (start + per_page).min(total);
|
||||
|
||||
let comments = if start < total {
|
||||
comments[start..end].to_vec()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(Json(VideoCommentsResponse {
|
||||
comments,
|
||||
page,
|
||||
per_page,
|
||||
total,
|
||||
pages,
|
||||
}))
|
||||
}
|
||||
Err(status) => Err(status),
|
||||
}
|
||||
}
|
2
src/routes/mod.rs
Executable file
2
src/routes/mod.rs
Executable file
@ -0,0 +1,2 @@
|
||||
pub mod comment;
|
||||
pub mod video;
|
@ -40,7 +40,7 @@ pub async fn list_videos(
|
||||
|
||||
let per_page = state.config.videos_per_page;
|
||||
let total = videos.len();
|
||||
let pages = (total + per_page - 1).div_ceil(per_page);
|
||||
let pages = total.div_ceil(per_page);
|
||||
let page = query.page.unwrap_or(1).max(1).min(pages);
|
||||
|
||||
let start = per_page * (page - 1);
|
||||
@ -64,9 +64,9 @@ pub async fn list_videos(
|
||||
/// Get a single video from the database by its ID
|
||||
pub async fn get_video(
|
||||
State(state): State<Instance>,
|
||||
Path(id): Path<i64>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<Video>, StatusCode> {
|
||||
sqlx::query_as!(Video, "SELECT * FROM video WHERE id = ?", id)
|
||||
sqlx::query_as!(Video, "SELECT * FROM video WHERE youtube_id = ?", id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.map_or(Err(StatusCode::INTERNAL_SERVER_ERROR), |video| {
|
||||
@ -77,7 +77,6 @@ pub async fn get_video(
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UploadVideoQuery {
|
||||
url: String,
|
||||
cookie: Option<String>,
|
||||
}
|
||||
|
||||
/// Upload a video to the database
|
||||
@ -96,9 +95,7 @@ pub async fn upload_video(
|
||||
}
|
||||
};
|
||||
|
||||
let new_video = Video::from_url(&query.url, id, query.cookie.as_deref())
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
let new_video = Video::from_url(&query.url, id).await.map_err(|e| match e {
|
||||
VideoError::InvalidUrl | VideoError::UrlParse(_) => StatusCode::BAD_REQUEST,
|
||||
VideoError::AlreadyExists => StatusCode::OK,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
18
src/video.rs
18
src/video.rs
@ -1,10 +1,11 @@
|
||||
use std::{fs, io, path::Path};
|
||||
use std::{io, path::Path};
|
||||
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use sha3::{Digest, Sha3_256};
|
||||
use thiserror::Error;
|
||||
use tokio::fs;
|
||||
use tracing::{error, info, warn};
|
||||
use url::{ParseError, Url};
|
||||
|
||||
@ -48,7 +49,7 @@ pub struct Video {
|
||||
}
|
||||
|
||||
impl Video {
|
||||
async fn yt_dlp_task(url: &str, cookie: Option<&str>) -> Result<(), VideoError> {
|
||||
async fn yt_dlp_task(url: &str) -> Result<(), VideoError> {
|
||||
let mut args = vec![
|
||||
"--write-info-json",
|
||||
"--write-thumbnail",
|
||||
@ -60,9 +61,6 @@ impl Video {
|
||||
"-v",
|
||||
url,
|
||||
];
|
||||
if let Some(cookie) = cookie {
|
||||
args.append(&mut vec!["--cookies", cookie]);
|
||||
}
|
||||
args.append(&mut vec![
|
||||
"-o",
|
||||
"videos/%(id)s/%(id)s.%(ext)s",
|
||||
@ -97,7 +95,7 @@ impl Video {
|
||||
Some(query_v.1.to_string())
|
||||
}
|
||||
|
||||
pub async fn from_url(url: &str, id: i64, cookie: Option<&str>) -> Result<Self, VideoError> {
|
||||
pub async fn from_url(url: &str, id: i64) -> Result<Self, VideoError> {
|
||||
let url = Url::parse(url)?;
|
||||
info!("Parsed argument as URL");
|
||||
|
||||
@ -112,7 +110,7 @@ impl Video {
|
||||
let dir = format!("videos/{youtube_id}");
|
||||
let file_stem = format!("{dir}/{youtube_id}");
|
||||
if !Path::new(&dir).exists() {
|
||||
fs::create_dir(dir)?;
|
||||
fs::create_dir(dir).await?;
|
||||
}
|
||||
|
||||
let info_json = format!("{file_stem}.info.json");
|
||||
@ -124,9 +122,9 @@ impl Video {
|
||||
return Err(VideoError::AlreadyExists);
|
||||
}
|
||||
|
||||
Self::yt_dlp_task(url.as_str(), cookie).await?;
|
||||
Self::yt_dlp_task(url.as_str()).await?;
|
||||
|
||||
let info: Value = serde_json::from_str(&fs::read_to_string(info_json)?)?;
|
||||
let info: Value = serde_json::from_str(&fs::read_to_string(info_json).await?)?;
|
||||
|
||||
// info!("Info JSON for {youtube_id}\n{info}");
|
||||
|
||||
@ -163,7 +161,7 @@ impl Video {
|
||||
let path = Path::new(&file_name);
|
||||
if path.exists() {
|
||||
info!("File '{}' found", path.display());
|
||||
buffer = Some(fs::read(path)?);
|
||||
buffer = Some(fs::read(path).await?);
|
||||
break;
|
||||
}
|
||||
warn!("File '{}' not found", path.display());
|
||||
|
Loading…
x
Reference in New Issue
Block a user