feat: complete modernization with shadcn, stateless auth, and performance optimizations
All checks were successful
Build MIPS Binary / build (push) Successful in 5m20s

This commit is contained in:
spinline
2026-02-10 22:16:36 +03:00
parent 8815727620
commit fddc81365b
18 changed files with 1150 additions and 2615 deletions

View File

@@ -16,7 +16,7 @@ tower-http = { version = "0.6", features = ["fs", "trace", "cors", "compression-
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rmp-serde = "1.3"
struct_patch = "0.5"
struct-patch = "0.5"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tokio-stream = "0.1"

View File

@@ -1,6 +1,5 @@
use std::collections::HashMap;
use shared::{AppEvent, NotificationLevel, SystemNotification, Torrent};
use struct_patch::traits::Patchable;
use shared::{AppEvent, NotificationLevel, SystemNotification, Torrent, TorrentUpdate};
#[derive(Debug)]
pub enum DiffResult {
@@ -27,18 +26,36 @@ pub fn diff_torrents(old: &[Torrent], new: &[Torrent]) -> DiffResult {
for new_t in new {
let old_t = old_map.get(new_t.hash.as_str()).unwrap();
// struct_patch::diff uses the Patch trait we derived in shared crate
let patch = old_t.diff(new_t);
// Manuel diff creating TorrentUpdate (which is the Patch struct)
let mut patch = TorrentUpdate::default();
let mut has_changes = false;
if !patch.is_empty() {
// If percent_complete jumped to 100, send notification
if old_t.name != new_t.name { patch.name = Some(new_t.name.clone()); has_changes = true; }
if old_t.size != new_t.size { patch.size = Some(new_t.size); has_changes = true; }
if old_t.down_rate != new_t.down_rate { patch.down_rate = Some(new_t.down_rate); has_changes = true; }
if old_t.up_rate != new_t.up_rate { patch.up_rate = Some(new_t.up_rate); has_changes = true; }
if old_t.completed != new_t.completed { patch.completed = Some(new_t.completed); has_changes = true; }
if old_t.eta != new_t.eta { patch.eta = Some(new_t.eta); has_changes = true; }
if (old_t.percent_complete - new_t.percent_complete).abs() > 0.01 {
patch.percent_complete = Some(new_t.percent_complete);
has_changes = true;
if old_t.percent_complete < 100.0 && new_t.percent_complete >= 100.0 {
tracing::info!("Torrent completed: {} ({})", new_t.name, new_t.hash);
events.push(AppEvent::Notification(SystemNotification {
level: NotificationLevel::Success,
message: format!("Torrent tamamlandı: {}", new_t.name),
}));
}
}
if old_t.status != new_t.status { patch.status = Some(new_t.status.clone()); has_changes = true; }
if old_t.error_message != new_t.error_message { patch.error_message = Some(new_t.error_message.clone()); has_changes = true; }
if old_t.label != new_t.label { patch.label = Some(new_t.label.clone()); has_changes = true; }
if has_changes {
// Set the hash (not an Option in Patch usually, but check shared/src/lib.rs)
// Wait, TorrentUpdate is a Patch, does it have 'hash' field?
// Yes, because Torrent has 'hash' field.
patch.hash = Some(new_t.hash.clone());
events.push(AppEvent::Update(patch));
}
}

View File

@@ -162,17 +162,7 @@ async fn main() {
// Initialize Database
tracing::info!("Connecting to database: {}", args.db_url);
// Ensure the db file exists if it's sqlite
if args.db_url.starts_with("sqlite:") {
let path = args.db_url.trim_start_matches("sqlite:");
if !std::path::Path::new(path).exists() {
tracing::info!("Database file not found, creating: {}", path);
match std::fs::File::create(path) {
Ok(_) => tracing::info!("Created empty database file"),
Err(e) => tracing::error!("Failed to create database file: {}", e),
}
}
}
// Redundant manual creation removed, shared::db handles it
let db: shared::db::Db = match shared::db::Db::new(&args.db_url).await {
Ok(db) => db,
@@ -440,8 +430,6 @@ async fn main() {
let scgi_path_for_ctx = args.socket.clone();
let db_for_ctx = db.clone();
let app = app
.route("/api/setup/status", get(handlers::setup::get_setup_status_handler))
.route("/api/setup", post(handlers::setup::setup_handler))
.route("/api/events", get(sse::sse_handler))
.route("/api/server_fns/{*fn_name}", post({
let scgi_path = scgi_path_for_ctx.clone();

View File

@@ -8,6 +8,8 @@ use futures::stream::{self, Stream};
use shared::{AppEvent, GlobalStats, Torrent, TorrentStatus};
use std::convert::Infallible;
use tokio_stream::StreamExt;
use axum::response::IntoResponse;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
// Field definitions to keep query and parser in sync
mod fields {
@@ -192,22 +194,6 @@ pub async fn fetch_global_stats(client: &RtorrentClient) -> Result<GlobalStats,
})
}
use shared::xmlrpc::{
parse_i64_response, parse_multicall_response, RpcParam, RtorrentClient, XmlRpcError,
};
use crate::AppState;
use axum::extract::State;
use axum::response::sse::{Event, Sse};
use futures::stream::{self, Stream};
use shared::{AppEvent, GlobalStats, Torrent, TorrentStatus};
use std::convert::Infallible;
use tokio_stream::StreamExt;
use axum::response::IntoResponse;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
// ... (fields and other helper functions remain the same)
pub async fn sse_handler(
State(state): State<AppState>,
) -> impl IntoResponse {
@@ -267,4 +253,4 @@ pub async fn sse_handler(
[("content-type", "application/x-msgpack")],
sse
)
}
}