Write OpenAPI documentation
This commit is contained in:
parent
9247a39094
commit
bc3469fa88
209
Cargo.lock
generated
209
Cargo.lock
generated
@ -49,7 +49,17 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"uuid",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -178,6 +188,12 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@ -244,6 +260,15 @@ version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
@ -280,6 +305,17 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@ -362,6 +398,16 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
@ -792,6 +838,7 @@ checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -863,6 +910,12 @@ dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lockfree-object-pool"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
@ -897,6 +950,16 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.8"
|
||||
@ -1199,6 +1262,40 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5fbc0ee50fcb99af7cebb442e5df7b5b45e9460ffa3f8f549cd26b862bec49d"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
@ -1230,6 +1327,15 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@ -1365,6 +1471,12 @@ dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
@ -1886,6 +1998,12 @@ version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
@ -1937,12 +2055,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.16.0"
|
||||
name = "utoipa"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa-gen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-gen"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-swagger-ui"
|
||||
version = "9.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29519b3c485df6b13f4478ac909a491387e9ef70204487c3b64b53749aec0be"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64",
|
||||
"mime_guess",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"utoipa",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1963,6 +2114,16 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
@ -2010,6 +2171,15 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
@ -2286,3 +2456,32 @@ dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"memchr",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"lockfree-object-pool",
|
||||
"log",
|
||||
"once_cell",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
@ -22,4 +22,5 @@ toml = "0.8.20"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
url = "2.5.4"
|
||||
uuid = { version = "1.16.0", features = ["v4"] }
|
||||
utoipa = { version = "5.3.1", features = ["axum_extras"] }
|
||||
utoipa-swagger-ui = { version = "9.0.1", features = ["axum"] }
|
||||
|
7
TODO.md
7
TODO.md
@ -4,5 +4,10 @@ Tasks that have been completed in commits before this one have been omitted from
|
||||
|
||||
- [x] /video PUT route
|
||||
- [x] /channel PUT route
|
||||
- [ ] OpenAPI documentation
|
||||
- [x] OpenAPI documentation
|
||||
|
||||
## Planned features
|
||||
|
||||
- [ ] Video versioning
|
||||
- [ ] Subtitles support
|
||||
- [ ] Cookies support
|
||||
|
@ -6,6 +6,7 @@ use thiserror::Error;
|
||||
use tokio::{fs, process::Command};
|
||||
use tracing::{error, info, warn};
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::string::ToUnquotedString;
|
||||
|
||||
@ -29,7 +30,7 @@ pub enum ChannelError {
|
||||
JsonKey(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct Channel {
|
||||
pub id: i64,
|
||||
pub url: String,
|
||||
|
@ -5,6 +5,7 @@ use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
use tokio::fs;
|
||||
use tracing::{error, warn};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::string::ToUnquotedString;
|
||||
|
||||
@ -19,7 +20,7 @@ pub enum CommentsError {
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct Comment {
|
||||
pub id: String,
|
||||
pub video_id: String,
|
||||
|
45
src/doc.rs
Executable file
45
src/doc.rs
Executable file
@ -0,0 +1,45 @@
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
// Because apparently the OpenApi macro can't import these on its own
|
||||
// if the routes are not in the same location as the router
|
||||
use crate::routes::{
|
||||
__path_index,
|
||||
channel::{
|
||||
__path_delete_channel, __path_get_channel, __path_get_channel_videos, __path_list_channels,
|
||||
__path_update_channel, __path_upload_channel,
|
||||
},
|
||||
comment::__path_get_video_comments,
|
||||
video::{
|
||||
__path_delete_video, __path_get_video, __path_list_videos, __path_update_video,
|
||||
__path_upload_video,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
index,
|
||||
list_channels,
|
||||
list_videos,
|
||||
upload_video,
|
||||
upload_channel,
|
||||
update_channel,
|
||||
update_video,
|
||||
delete_channel,
|
||||
delete_video,
|
||||
get_video,
|
||||
get_channel,
|
||||
get_video_comments,
|
||||
get_channel_videos
|
||||
),
|
||||
info(
|
||||
title = "Almond API",
|
||||
description = "Interface to archive YouTube videos."
|
||||
)
|
||||
)]
|
||||
struct ApiDoc;
|
||||
|
||||
pub fn docs_router() -> SwaggerUi {
|
||||
SwaggerUi::new("/docs").url("/api-doc/openapi.json", ApiDoc::openapi())
|
||||
}
|
15
src/main.rs
15
src/main.rs
@ -4,13 +4,18 @@ use axum::{
|
||||
Router,
|
||||
routing::{delete, get, post, put},
|
||||
};
|
||||
use doc::docs_router;
|
||||
use instance::Instance;
|
||||
|
||||
use routes::{
|
||||
channel::{delete_channel, get_channel, list_channels, update_channel, upload_channel},
|
||||
channel::{
|
||||
delete_channel, get_channel, get_channel_videos, list_channels, update_channel,
|
||||
upload_channel,
|
||||
},
|
||||
comment::get_video_comments,
|
||||
index,
|
||||
middleware::auth,
|
||||
video::{delete_video, get_channel_videos, get_video, list_videos, update_video, upload_video},
|
||||
video::{delete_video, get_video, list_videos, update_video, upload_video},
|
||||
};
|
||||
use tokio::signal;
|
||||
use tracing::info;
|
||||
@ -18,6 +23,7 @@ use tracing::info;
|
||||
mod channel;
|
||||
mod comment;
|
||||
mod config;
|
||||
mod doc;
|
||||
mod instance;
|
||||
mod routes;
|
||||
mod string;
|
||||
@ -60,7 +66,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.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 almond = router
|
||||
.merge(protected)
|
||||
.merge(docs_router())
|
||||
.with_state(instance);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(address).await?;
|
||||
|
||||
|
@ -6,19 +6,21 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, info};
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
channel::{Channel, ChannelError},
|
||||
instance::Instance,
|
||||
url::is_youtube_url,
|
||||
video::Video,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ListChannelsQuery {
|
||||
page: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListChannelsResponse {
|
||||
channels: Vec<Channel>,
|
||||
page: usize,
|
||||
@ -28,6 +30,7 @@ pub struct ListChannelsResponse {
|
||||
}
|
||||
|
||||
/// Retrieve video list as JSON (paged)
|
||||
#[utoipa::path(get, path = "/channel", request_body = ListChannelsQuery, params(("page" = Option<usize>, Query)), tags = ["channel"], responses((status = 200, body = ListChannelsResponse), (status = 500, description = "Error fetching channels from database")))]
|
||||
pub async fn list_channels(
|
||||
State(state): State<Instance>,
|
||||
Query(query): Query<ListChannelsQuery>,
|
||||
@ -64,6 +67,7 @@ pub async fn list_channels(
|
||||
}
|
||||
|
||||
/// Get a single channel from the database by its ID
|
||||
#[utoipa::path(get, path = "/channel/{id}", tags = ["channel"], responses((status = 200, body = Channel), (status = 404, description = "Not found"), (status = 500, description = "Failed to get channel from database")))]
|
||||
pub async fn get_channel(
|
||||
State(state): State<Instance>,
|
||||
Path(id): Path<String>,
|
||||
@ -76,12 +80,31 @@ pub async fn get_channel(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
/// Get all videos belonging to a channel ID from the database
|
||||
#[utoipa::path(get, path = "/channel/{id}/videos", tags = ["channel"], responses((status = 200, body = Vec<Video>), (status = 404, description = "Not found"), (status = 500, description = "Error fetching videos from 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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct UploadChannelQuery {
|
||||
url: String,
|
||||
}
|
||||
|
||||
/// Upload a channel's metadata to the database
|
||||
#[utoipa::path(post, path = "/channel", request_body = UploadChannelQuery, params(("url" = String, Query), ("almond-api-key" = String, description = "API key")), tags = ["channel"], responses((status = 200, description = "Channel already exists, skipping"), (status = 201, description = "Channel uploaded successfully"), (status = 400, description = "Bad request, likely a malformed URL"), (status = 401, description = "Unauthorized"), (status = 500, description = "Failed to upload channel to database")))]
|
||||
pub async fn upload_channel(
|
||||
State(state): State<Instance>,
|
||||
Query(query): Query<UploadChannelQuery>,
|
||||
@ -141,7 +164,7 @@ pub async fn upload_channel(
|
||||
{
|
||||
Ok(result) => {
|
||||
info!("Inserted channel to database successfully! {result:?}");
|
||||
StatusCode::OK
|
||||
StatusCode::CREATED
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error inserting channel to database: {e:?}");
|
||||
@ -154,6 +177,7 @@ pub async fn upload_channel(
|
||||
}
|
||||
|
||||
/// Update an existing channel from the database
|
||||
#[utoipa::path(put, path = "/channel/{id}", params(("almond-api-key" = String, description = "API key")), tags = ["channel"], responses((status = 204, description = "Channel updated successfully"), (status = 404, description = "Video with specified ID not found"), (status = 500, description = "Failed to update channel from 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)
|
||||
@ -175,7 +199,7 @@ pub async fn update_channel(State(state): State<Instance>, Path(id): Path<String
|
||||
).execute(&state.pool).await {
|
||||
Ok(r) => {
|
||||
info!("Updated channel '{id}' successfully: {r:?}");
|
||||
StatusCode::OK
|
||||
StatusCode::NO_CONTENT
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Update channel failed: {e:?}");
|
||||
@ -198,6 +222,7 @@ pub async fn update_channel(State(state): State<Instance>, Path(id): Path<String
|
||||
}
|
||||
|
||||
/// Delete a channel from the database
|
||||
#[utoipa::path(delete, path = "/channel/{id}", params(("almond-api-key" = String, description = "API key")), tags = ["channel"], responses((status = 204, description = "Channel deleted successfully"), (status = 404, description = "Video with specified ID not found"), (status = 500, description = "Failed to delete channel from 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)
|
||||
|
@ -5,6 +5,7 @@ use axum::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::error;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
comment::{Comment, CommentsError, get_comments_from_video},
|
||||
@ -16,7 +17,7 @@ pub struct VideoCommentsQuery {
|
||||
page: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct VideoCommentsResponse {
|
||||
comments: Vec<Comment>,
|
||||
page: usize,
|
||||
@ -26,6 +27,7 @@ pub struct VideoCommentsResponse {
|
||||
}
|
||||
|
||||
/// Fetches the comments from a video, will return an empty vec if the video has no comments
|
||||
#[utoipa::path(get, path = "/video/{id}/comments", tags = ["comment"], responses((status = 200, body = VideoCommentsResponse), (status = 404, description = "Not found"), (status = 500, description = "Failed to get video comments")))]
|
||||
pub async fn get_video_comments(
|
||||
State(state): State<Instance>,
|
||||
Path(id): Path<String>,
|
||||
|
@ -1,18 +1,20 @@
|
||||
use axum::Json;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub mod channel;
|
||||
pub mod comment;
|
||||
pub mod middleware;
|
||||
pub mod video;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct IndexResponse {
|
||||
app_name: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
/// Get general information from the current Almond instance
|
||||
#[utoipa::path(get, path = "/", tags = ["application"], responses((status = 200, body = IndexResponse)))]
|
||||
pub async fn index() -> Json<IndexResponse> {
|
||||
let app_name = "Almond".into();
|
||||
let version = env!("CARGO_PKG_VERSION").into();
|
||||
|
@ -6,6 +6,7 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, info};
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
instance::Instance,
|
||||
@ -13,12 +14,12 @@ use crate::{
|
||||
video::{Video, VideoError},
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ListVideosQuery {
|
||||
page: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListVideosResponse {
|
||||
videos: Vec<Video>,
|
||||
page: usize,
|
||||
@ -28,6 +29,7 @@ pub struct ListVideosResponse {
|
||||
}
|
||||
|
||||
/// Retrieve video list as JSON (paged)
|
||||
#[utoipa::path(get, path = "/video", request_body = ListVideosQuery, params(("page" = Option<usize>, Query)), tags = ["video"], responses((status = 200, body = ListVideosResponse), (status = 500, description = "Error fetching videos from database")))]
|
||||
pub async fn list_videos(
|
||||
State(state): State<Instance>,
|
||||
Query(query): Query<ListVideosQuery>,
|
||||
@ -63,24 +65,8 @@ 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
|
||||
#[utoipa::path(get, path = "/video/{id}", tags = ["video"], responses((status = 200, body = Video), (status = 404, description = "Not found"), (status = 500, description = "Failed to get video from database")))]
|
||||
pub async fn get_video(
|
||||
State(state): State<Instance>,
|
||||
Path(id): Path<String>,
|
||||
@ -93,12 +79,13 @@ pub async fn get_video(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct UploadVideoQuery {
|
||||
url: String,
|
||||
}
|
||||
|
||||
/// Upload a video to the database
|
||||
#[utoipa::path(post, path = "/video", request_body = UploadVideoQuery, params(("url" = String, Query), ("almond-api-key" = String, description = "API key")), tags = ["video"], responses((status = 200, description = "Video already exists, skipping"), (status = 201, description = "Video uploaded successfully"), (status = 400, description = "Bad request, likely a malformed URL"), (status = 401, description = "Unauthorized"), (status = 500, description = "Failed to upload video to database")))]
|
||||
pub async fn upload_video(
|
||||
State(state): State<Instance>,
|
||||
Query(query): Query<UploadVideoQuery>,
|
||||
@ -162,7 +149,7 @@ pub async fn upload_video(
|
||||
{
|
||||
Ok(result) => {
|
||||
info!("Inserted video to database successfully! {result:?}");
|
||||
StatusCode::OK
|
||||
StatusCode::CREATED
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error inserting video to database: {e:?}");
|
||||
@ -175,6 +162,7 @@ pub async fn upload_video(
|
||||
}
|
||||
|
||||
/// Update an existing video from the database
|
||||
#[utoipa::path(put, path = "/video/{id}", params(("almond-api-key" = String, description = "API key")), tags = ["video"], responses((status = 204, description = "Video updated successfully"), (status = 404, description = "Video with specified ID not found"), (status = 500, description = "Failed to update video from 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)
|
||||
@ -194,7 +182,7 @@ pub async fn update_video(State(state): State<Instance>, Path(id): Path<String>)
|
||||
).execute(&state.pool).await {
|
||||
Ok(r) => {
|
||||
info!("Updated video '{id}' successfully: {r:?}");
|
||||
StatusCode::OK
|
||||
StatusCode::NO_CONTENT
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Update video failed: {e:?}");
|
||||
@ -217,6 +205,7 @@ pub async fn update_video(State(state): State<Instance>, Path(id): Path<String>)
|
||||
}
|
||||
|
||||
/// Delete a video from the database
|
||||
#[utoipa::path(delete, path = "/video/{id}", params(("almond-api-key" = String, description = "API key")), tags = ["video"], responses((status = 204, description = "Video deleted successfully"), (status = 404, description = "Video with specified ID not found"), (status = 500, description = "Failed to delete video from 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)
|
||||
@ -226,7 +215,7 @@ pub async fn delete_video(State(state): State<Instance>, Path(id): Path<String>)
|
||||
}) {
|
||||
Ok(_) => {
|
||||
info!("Deleted video '{id}' successfully");
|
||||
StatusCode::OK
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
Err(status) => status,
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use thiserror::Error;
|
||||
use tokio::fs;
|
||||
use tracing::{error, info, warn};
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::string::ToUnquotedString;
|
||||
|
||||
@ -28,7 +29,7 @@ pub enum VideoError {
|
||||
MissingVideoFile,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct Video {
|
||||
pub id: i64,
|
||||
pub url: String,
|
||||
|
Loading…
x
Reference in New Issue
Block a user