feat: add api docs and refactor AppEvent to struct variant
This commit is contained in:
@@ -4,7 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.7", features = ["macros", "ws"] }
|
axum = { version = "0.8", features = ["macros", "ws"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tower = { version = "0.4", features = ["util", "timeout"] }
|
tower = { version = "0.4", features = ["util", "timeout"] }
|
||||||
tower-http = { version = "0.5", features = ["fs", "trace", "cors", "compression-full"] }
|
tower-http = { version = "0.5", features = ["fs", "trace", "cors", "compression-full"] }
|
||||||
@@ -24,3 +24,5 @@ mime_guess = "2.0"
|
|||||||
shared = { path = "../shared" }
|
shared = { path = "../shared" }
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
|
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||||
|
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ use axum::{
|
|||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use shared::TorrentActionRequest;
|
use shared::TorrentActionRequest;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
#[derive(RustEmbed)]
|
||||||
#[folder = "../frontend/dist"]
|
#[folder = "../frontend/dist"]
|
||||||
pub struct Asset;
|
pub struct Asset;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, ToSchema)]
|
||||||
pub struct AddTorrentRequest {
|
pub struct AddTorrentRequest {
|
||||||
|
/// Magnet link or Torrent file URL
|
||||||
|
#[schema(example = "magnet:?xt=urn:btih:...")]
|
||||||
uri: String,
|
uri: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +49,16 @@ pub async fn static_handler(uri: Uri) -> impl IntoResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a new torrent via magnet link or URL
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/torrents/add",
|
||||||
|
request_body = AddTorrentRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Torrent added successfully"),
|
||||||
|
(status = 500, description = "Internal server error or rTorrent fault")
|
||||||
|
)
|
||||||
|
)]
|
||||||
pub async fn add_torrent_handler(
|
pub async fn add_torrent_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(payload): Json<AddTorrentRequest>,
|
Json(payload): Json<AddTorrentRequest>,
|
||||||
@@ -193,6 +206,18 @@ async fn delete_torrent_with_data(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform an action on a torrent (start, stop, delete)
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/torrents/action",
|
||||||
|
request_body = TorrentActionRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Action executed successfully"),
|
||||||
|
(status = 400, description = "Invalid action or request"),
|
||||||
|
(status = 403, description = "Forbidden: Security risk detected"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
pub async fn handle_torrent_action(
|
pub async fn handle_torrent_action(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(payload): Json<TorrentActionRequest>,
|
Json(payload): Json<TorrentActionRequest>,
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ use tower_http::{
|
|||||||
cors::CorsLayer,
|
cors::CorsLayer,
|
||||||
trace::TraceLayer,
|
trace::TraceLayer,
|
||||||
};
|
};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
@@ -47,6 +49,27 @@ struct Args {
|
|||||||
port: u16,
|
port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
handlers::add_torrent_handler,
|
||||||
|
handlers::handle_torrent_action
|
||||||
|
),
|
||||||
|
components(
|
||||||
|
schemas(
|
||||||
|
handlers::AddTorrentRequest,
|
||||||
|
shared::TorrentActionRequest,
|
||||||
|
shared::Torrent,
|
||||||
|
shared::TorrentStatus,
|
||||||
|
shared::Theme
|
||||||
|
)
|
||||||
|
),
|
||||||
|
tags(
|
||||||
|
(name = "vibetorrent", description = "VibeTorrent API")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
struct ApiDoc;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Load .env file
|
// Load .env file
|
||||||
@@ -122,8 +145,10 @@ async fn main() {
|
|||||||
|
|
||||||
match diff::diff_torrents(&previous_torrents, &new_torrents) {
|
match diff::diff_torrents(&previous_torrents, &new_torrents) {
|
||||||
diff::DiffResult::FullUpdate => {
|
diff::DiffResult::FullUpdate => {
|
||||||
let _ =
|
let _ = event_bus_tx.send(AppEvent::FullList {
|
||||||
event_bus_tx.send(AppEvent::FullList(new_torrents.clone(), now));
|
torrents: new_torrents.clone(),
|
||||||
|
timestamp: now,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
diff::DiffResult::Partial(updates) => {
|
diff::DiffResult::Partial(updates) => {
|
||||||
for update in updates {
|
for update in updates {
|
||||||
@@ -144,6 +169,7 @@ async fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()))
|
||||||
.route("/api/events", get(sse::sse_handler))
|
.route("/api/events", get(sse::sse_handler))
|
||||||
.route("/api/torrents/add", post(handlers::add_torrent_handler))
|
.route("/api/torrents/add", post(handlers::add_torrent_handler))
|
||||||
.route(
|
.route(
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use leptos::*;
|
|
||||||
use shared::{Torrent, AppEvent};
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gloo_net::eventsource::futures::EventSource;
|
use gloo_net::eventsource::futures::EventSource;
|
||||||
|
use leptos::*;
|
||||||
|
use shared::{AppEvent, Torrent};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum FilterStatus {
|
pub enum FilterStatus {
|
||||||
@@ -41,7 +40,11 @@ pub fn provide_torrent_store() {
|
|||||||
let filter = create_rw_signal(FilterStatus::All);
|
let filter = create_rw_signal(FilterStatus::All);
|
||||||
let search_query = create_rw_signal(String::new());
|
let search_query = create_rw_signal(String::new());
|
||||||
|
|
||||||
let store = TorrentStore { torrents, filter, search_query };
|
let store = TorrentStore {
|
||||||
|
torrents,
|
||||||
|
filter,
|
||||||
|
search_query,
|
||||||
|
};
|
||||||
provide_context(store);
|
provide_context(store);
|
||||||
|
|
||||||
// Initialize SSE connection
|
// Initialize SSE connection
|
||||||
@@ -52,28 +55,47 @@ pub fn provide_torrent_store() {
|
|||||||
|
|
||||||
while let Some(Ok((_, msg))) = stream.next().await {
|
while let Some(Ok((_, msg))) = stream.next().await {
|
||||||
if let Some(data_str) = msg.data().as_string() {
|
if let Some(data_str) = msg.data().as_string() {
|
||||||
if let Ok(event) = serde_json::from_str::<AppEvent>(&data_str) {
|
if let Ok(event) = serde_json::from_str::<AppEvent>(&data_str) {
|
||||||
match event {
|
match event {
|
||||||
AppEvent::FullList(list, _) => {
|
AppEvent::FullList { torrents: list, .. } => {
|
||||||
torrents.set(list);
|
torrents.set(list);
|
||||||
}
|
}
|
||||||
AppEvent::Update(update) => {
|
AppEvent::Update(update) => {
|
||||||
torrents.update(|list| {
|
torrents.update(|list| {
|
||||||
if let Some(t) = list.iter_mut().find(|t| t.hash == update.hash) {
|
if let Some(t) = list.iter_mut().find(|t| t.hash == update.hash)
|
||||||
if let Some(name) = update.name { t.name = name; }
|
{
|
||||||
if let Some(size) = update.size { t.size = size; }
|
if let Some(name) = update.name {
|
||||||
if let Some(down_rate) = update.down_rate { t.down_rate = down_rate; }
|
t.name = name;
|
||||||
if let Some(up_rate) = update.up_rate { t.up_rate = up_rate; }
|
}
|
||||||
if let Some(percent_complete) = update.percent_complete { t.percent_complete = percent_complete; }
|
if let Some(size) = update.size {
|
||||||
if let Some(completed) = update.completed { t.completed = completed; }
|
t.size = size;
|
||||||
if let Some(eta) = update.eta { t.eta = eta; }
|
}
|
||||||
if let Some(status) = update.status { t.status = status; }
|
if let Some(down_rate) = update.down_rate {
|
||||||
if let Some(error_message) = update.error_message { t.error_message = error_message; }
|
t.down_rate = down_rate;
|
||||||
|
}
|
||||||
|
if let Some(up_rate) = update.up_rate {
|
||||||
|
t.up_rate = up_rate;
|
||||||
|
}
|
||||||
|
if let Some(percent_complete) = update.percent_complete {
|
||||||
|
t.percent_complete = percent_complete;
|
||||||
|
}
|
||||||
|
if let Some(completed) = update.completed {
|
||||||
|
t.completed = completed;
|
||||||
|
}
|
||||||
|
if let Some(eta) = update.eta {
|
||||||
|
t.eta = eta;
|
||||||
|
}
|
||||||
|
if let Some(status) = update.status {
|
||||||
|
t.status = status;
|
||||||
|
}
|
||||||
|
if let Some(error_message) = update.error_message {
|
||||||
|
t.error_message = error_message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
|
||||||
pub struct Torrent {
|
pub struct Torrent {
|
||||||
pub hash: String,
|
pub hash: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -15,7 +16,7 @@ pub struct Torrent {
|
|||||||
pub added_date: i64,
|
pub added_date: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
|
||||||
pub enum TorrentStatus {
|
pub enum TorrentStatus {
|
||||||
Downloading,
|
Downloading,
|
||||||
Seeding,
|
Seeding,
|
||||||
@@ -25,14 +26,17 @@ pub enum TorrentStatus {
|
|||||||
Queued,
|
Queued,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
#[serde(tag = "type", content = "data")]
|
#[serde(tag = "type", content = "data")]
|
||||||
pub enum AppEvent {
|
pub enum AppEvent {
|
||||||
FullList(Vec<Torrent>, u64), // u64 is likely free_space_bytes
|
FullList {
|
||||||
|
torrents: Vec<Torrent>,
|
||||||
|
timestamp: u64,
|
||||||
|
},
|
||||||
Update(TorrentUpdate),
|
Update(TorrentUpdate),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
pub struct TorrentUpdate {
|
pub struct TorrentUpdate {
|
||||||
pub hash: String,
|
pub hash: String,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
@@ -46,14 +50,17 @@ pub struct TorrentUpdate {
|
|||||||
pub error_message: Option<String>,
|
pub error_message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
pub struct TorrentActionRequest {
|
pub struct TorrentActionRequest {
|
||||||
|
/// The hash of the torrent
|
||||||
|
#[schema(example = "5D4C9065...")]
|
||||||
pub hash: String,
|
pub hash: String,
|
||||||
pub action: String, // "start", "stop", "delete"
|
/// The action to perform: "start", "stop", "delete", "delete_with_data"
|
||||||
|
#[schema(example = "start")]
|
||||||
|
pub action: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Added Theme here to separate it from backend logic but allow frontend usage via shared
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
|
||||||
pub enum Theme {
|
pub enum Theme {
|
||||||
Midnight,
|
Midnight,
|
||||||
Light,
|
Light,
|
||||||
|
|||||||
Reference in New Issue
Block a user