Compare commits
5 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93a43d1b38 | ||
|
|
91ca6ff96f | ||
|
|
8c0d35cca5 | ||
|
|
444ea4326d | ||
|
|
401ccb69b2 |
83
backend/src/bin/test_trackers.rs
Normal file
83
backend/src/bin/test_trackers.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use shared::xmlrpc::{RtorrentClient, RpcParam, parse_multicall_response};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let mut client = None;
|
||||||
|
for path in ["127.0.0.1:8000", "0.0.0.0:8000", "localhost:8000"] {
|
||||||
|
let test_client = RtorrentClient::new(path);
|
||||||
|
match test_client.call("system.client_version", &[]).await {
|
||||||
|
Ok(res) => {
|
||||||
|
println!("SUCCESS: Connected to rTorrent at {} (Version: {})", path, res);
|
||||||
|
client = Some(test_client);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// println!("Failed to connect to {}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = match client {
|
||||||
|
Some(c) => c,
|
||||||
|
None => {
|
||||||
|
println!("Could not connect to rTorrent on port 8000.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut hash = String::new();
|
||||||
|
match client.call("d.multicall2", &[RpcParam::from(""), RpcParam::from("main"), RpcParam::from("d.hash=")]).await {
|
||||||
|
Ok(xml) => {
|
||||||
|
if let Ok(rows) = parse_multicall_response(&xml) {
|
||||||
|
if let Some(row) = rows.first() {
|
||||||
|
if let Some(h) = row.first() {
|
||||||
|
hash = h.clone();
|
||||||
|
println!("Using torrent hash: {}", hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error getting torrents: {:?}", e);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if hash.is_empty() {
|
||||||
|
println!("No torrents found to test trackers.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now test Tracker fields one by one to see which one is failing
|
||||||
|
let fields = vec![
|
||||||
|
"t.url=",
|
||||||
|
"t.is_enabled=",
|
||||||
|
"t.group=",
|
||||||
|
"t.scrape_complete=",
|
||||||
|
"t.scrape_incomplete=",
|
||||||
|
"t.scrape_downloaded=",
|
||||||
|
"t.activity_date_last=",
|
||||||
|
"t.normal_interval=",
|
||||||
|
"t.message=",
|
||||||
|
];
|
||||||
|
|
||||||
|
for field in &fields {
|
||||||
|
let params = vec![
|
||||||
|
RpcParam::from(hash.as_str()),
|
||||||
|
RpcParam::from(""),
|
||||||
|
RpcParam::from(*field),
|
||||||
|
];
|
||||||
|
|
||||||
|
print!("Testing field {:<22} : ", field);
|
||||||
|
match client.call("t.multicall", ¶ms).await {
|
||||||
|
Ok(xml) => {
|
||||||
|
if xml.contains("faultCode") {
|
||||||
|
println!("FAILED");
|
||||||
|
} else {
|
||||||
|
println!("SUCCESS");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => println!("ERROR: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,9 +51,9 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
|
|||||||
</SheetClose>
|
</SheetClose>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-hidden p-6">
|
<div class="flex-1 overflow-hidden flex flex-col pt-4 px-6 pb-0">
|
||||||
<Tabs default_value="general" class="h-full flex flex-col">
|
<Tabs default_value="general" class="flex-1 h-full min-h-0 flex flex-col">
|
||||||
<TabsList class="w-full justify-start rounded-none border-b bg-transparent p-0">
|
<TabsList class="w-full justify-start rounded-none border-b bg-transparent p-0 shrink-0">
|
||||||
<TabsTrigger value="general" class="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none">
|
<TabsTrigger value="general" class="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none">
|
||||||
"Genel"
|
"Genel"
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -68,7 +68,7 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto mt-4 pb-12">
|
<crate::components::ui::scroll_area::ScrollArea class="flex-1 min-h-0 mt-4 pb-12 pr-4">
|
||||||
<TabsContent value="general" class="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
<TabsContent value="general" class="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||||
<crate::components::ui::shimmer::Shimmer
|
<crate::components::ui::shimmer::Shimmer
|
||||||
loading=Signal::derive(move || selected_torrent.get().is_none())
|
loading=Signal::derive(move || selected_torrent.get().is_none())
|
||||||
@@ -151,7 +151,7 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
|
|||||||
<TabsContent value="files" class="h-full">
|
<TabsContent value="files" class="h-full">
|
||||||
{move || match selected_torrent.get() {
|
{move || match selected_torrent.get() {
|
||||||
Some(t) => leptos::either::Either::Left(view! {
|
Some(t) => leptos::either::Either::Left(view! {
|
||||||
<div class="h-full overflow-y-auto pr-2 pb-8">
|
<div class="h-full">
|
||||||
<crate::components::torrent::files::TorrentFilesTab hash=t.hash />
|
<crate::components::torrent::files::TorrentFilesTab hash=t.hash />
|
||||||
</div>
|
</div>
|
||||||
}),
|
}),
|
||||||
@@ -167,7 +167,7 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
|
|||||||
<TabsContent value="trackers" class="h-full">
|
<TabsContent value="trackers" class="h-full">
|
||||||
{move || match selected_torrent.get() {
|
{move || match selected_torrent.get() {
|
||||||
Some(t) => leptos::either::Either::Left(view! {
|
Some(t) => leptos::either::Either::Left(view! {
|
||||||
<div class="h-full overflow-y-auto pr-2 pb-8">
|
<div class="h-full">
|
||||||
<crate::components::torrent::trackers::TorrentTrackersTab hash=t.hash />
|
<crate::components::torrent::trackers::TorrentTrackersTab hash=t.hash />
|
||||||
</div>
|
</div>
|
||||||
}),
|
}),
|
||||||
@@ -186,7 +186,7 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
|
|||||||
<p class="text-sm font-medium">"Eş listesi yakında eklenecek"</p>
|
<p class="text-sm font-medium">"Eş listesi yakında eklenecek"</p>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</div>
|
</crate::components::ui::scroll_area::ScrollArea>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub mod input;
|
|||||||
pub mod multi_select;
|
pub mod multi_select;
|
||||||
pub mod select;
|
pub mod select;
|
||||||
pub mod separator;
|
pub mod separator;
|
||||||
|
pub mod scroll_area;
|
||||||
pub mod sheet;
|
pub mod sheet;
|
||||||
pub mod sidenav;
|
pub mod sidenav;
|
||||||
pub mod skeleton;
|
pub mod skeleton;
|
||||||
|
|||||||
131
frontend/src/components/ui/scroll_area.rs
Normal file
131
frontend/src/components/ui/scroll_area.rs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_ui::void;
|
||||||
|
use tw_merge::*;
|
||||||
|
|
||||||
|
mod components {
|
||||||
|
use super::*;
|
||||||
|
void! {ScrollAreaThumb, div, "bg-border relative flex-1 rounded-full"}
|
||||||
|
void! {ScrollAreaCorner, div, "bg-border"}
|
||||||
|
}
|
||||||
|
pub use components::*;
|
||||||
|
|
||||||
|
/* ========================================================== */
|
||||||
|
/* ✨ COMPONENTS ✨ */
|
||||||
|
/* ========================================================== */
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ScrollArea(children: Children, #[prop(into, optional)] class: String) -> impl IntoView {
|
||||||
|
let merged_class = tw_merge!("relative overflow-hidden", class);
|
||||||
|
view! {
|
||||||
|
<div data-name="ScrollArea" class=merged_class>
|
||||||
|
<ScrollAreaViewport>{children()}</ScrollAreaViewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaCorner />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ScrollAreaViewport(children: Children, #[prop(into, optional)] class: String) -> impl IntoView {
|
||||||
|
let merged_class = tw_merge!(
|
||||||
|
"focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 overflow-auto",
|
||||||
|
class
|
||||||
|
);
|
||||||
|
view! {
|
||||||
|
<div data-name="ScrollAreaViewport" class=merged_class>
|
||||||
|
{children()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================================== */
|
||||||
|
/* 🧬 ENUMS 🧬 */
|
||||||
|
/* ========================================================== */
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
pub enum ScrollBarOrientation {
|
||||||
|
#[default]
|
||||||
|
Vertical,
|
||||||
|
Horizontal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ScrollBar(
|
||||||
|
#[prop(default = ScrollBarOrientation::default())] orientation: ScrollBarOrientation,
|
||||||
|
#[prop(into, optional)] class: String,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let orientation_class = match orientation {
|
||||||
|
ScrollBarOrientation::Vertical => "h-full w-2.5 border-l border-l-transparent",
|
||||||
|
ScrollBarOrientation::Horizontal => "h-2.5 flex-col border-t border-t-transparent",
|
||||||
|
};
|
||||||
|
let merged_class = tw_merge!("flex touch-none p-px transition-colors select-none", orientation_class, class);
|
||||||
|
view! {
|
||||||
|
<div data-name="ScrollBar" class=merged_class>
|
||||||
|
<ScrollAreaThumb />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================================== */
|
||||||
|
/* 🧬 STRUCT 🧬 */
|
||||||
|
/* ========================================================== */
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SnapScrollArea(
|
||||||
|
#[prop(into, default = SnapAreaVariant::default())] variant: SnapAreaVariant,
|
||||||
|
#[prop(into, optional)] class: String,
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let snap_item = SnapAreaClass { variant };
|
||||||
|
let merged_class = snap_item.with_class(class);
|
||||||
|
view! {
|
||||||
|
<div data-name="SnapScrollArea" class=merged_class>
|
||||||
|
{children()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TwClass, Default)]
|
||||||
|
#[tw(class = "")]
|
||||||
|
pub struct SnapAreaClass {
|
||||||
|
variant: SnapAreaVariant,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TwVariant)]
|
||||||
|
pub enum SnapAreaVariant {
|
||||||
|
// * snap-x by default
|
||||||
|
#[tw(default, class = "overflow-x-auto snap-x")]
|
||||||
|
Center,
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================================== */
|
||||||
|
/* 🧬 STRUCT 🧬 */
|
||||||
|
/* ========================================================== */
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SnapItem(
|
||||||
|
#[prop(into, default = SnapVariant::default())] variant: SnapVariant,
|
||||||
|
#[prop(into, optional)] class: String,
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let snap_item = SnapItemClass { variant };
|
||||||
|
let merged_class = snap_item.with_class(class);
|
||||||
|
view! {
|
||||||
|
<div data-name="SnapItem" class=merged_class>
|
||||||
|
{children()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TwClass, Default)]
|
||||||
|
#[tw(class = "shrink-0")]
|
||||||
|
pub struct SnapItemClass {
|
||||||
|
variant: SnapVariant,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TwVariant)]
|
||||||
|
pub enum SnapVariant {
|
||||||
|
// * snap-center by default
|
||||||
|
#[tw(default, class = "snap-center")]
|
||||||
|
Center,
|
||||||
|
}
|
||||||
@@ -83,12 +83,19 @@ impl ScgiRequest {
|
|||||||
|
|
||||||
pub async fn send_request(socket_path: &str, request: ScgiRequest) -> Result<Bytes, ScgiError> {
|
pub async fn send_request(socket_path: &str, request: ScgiRequest) -> Result<Bytes, ScgiError> {
|
||||||
let perform_request = async {
|
let perform_request = async {
|
||||||
let mut stream = UnixStream::connect(socket_path).await?;
|
|
||||||
let data = request.encode();
|
let data = request.encode();
|
||||||
stream.write_all(&data).await?;
|
|
||||||
|
|
||||||
let mut response = Vec::new();
|
let mut response = Vec::new();
|
||||||
stream.read_to_end(&mut response).await?;
|
|
||||||
|
if socket_path.contains(':') {
|
||||||
|
let mut stream = tokio::net::TcpStream::connect(socket_path).await?;
|
||||||
|
stream.write_all(&data).await?;
|
||||||
|
stream.read_to_end(&mut response).await?;
|
||||||
|
} else {
|
||||||
|
let mut stream = tokio::net::UnixStream::connect(socket_path).await?;
|
||||||
|
stream.write_all(&data).await?;
|
||||||
|
stream.read_to_end(&mut response).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok::<Vec<u8>, std::io::Error>(response)
|
Ok::<Vec<u8>, std::io::Error>(response)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -198,9 +198,7 @@ pub async fn get_trackers(hash: String) -> Result<Vec<TorrentTracker>, ServerFnE
|
|||||||
RpcParam::from("t.scrape_complete="),
|
RpcParam::from("t.scrape_complete="),
|
||||||
RpcParam::from("t.scrape_incomplete="),
|
RpcParam::from("t.scrape_incomplete="),
|
||||||
RpcParam::from("t.scrape_downloaded="),
|
RpcParam::from("t.scrape_downloaded="),
|
||||||
RpcParam::from("t.activity_date_last="),
|
|
||||||
RpcParam::from("t.normal_interval="),
|
RpcParam::from("t.normal_interval="),
|
||||||
RpcParam::from("t.message="),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let xml = client
|
let xml = client
|
||||||
@@ -211,7 +209,7 @@ pub async fn get_trackers(hash: String) -> Result<Vec<TorrentTracker>, ServerFnE
|
|||||||
let rows = parse_multicall_response(&xml)
|
let rows = parse_multicall_response(&xml)
|
||||||
.map_err(|e| ServerFnError::new(format!("Parse error: {}", e)))?;
|
.map_err(|e| ServerFnError::new(format!("Parse error: {}", e)))?;
|
||||||
|
|
||||||
Ok(rows
|
let result: Vec<TorrentTracker> = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| TorrentTracker {
|
.map(|row| TorrentTracker {
|
||||||
url: row.get(0).cloned().unwrap_or_default(),
|
url: row.get(0).cloned().unwrap_or_default(),
|
||||||
@@ -220,12 +218,14 @@ pub async fn get_trackers(hash: String) -> Result<Vec<TorrentTracker>, ServerFnE
|
|||||||
seeders: row.get(3).and_then(|s| s.parse().ok()).unwrap_or(0),
|
seeders: row.get(3).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
peers: row.get(4).and_then(|s| s.parse().ok()).unwrap_or(0),
|
peers: row.get(4).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
downloaded: row.get(5).and_then(|s| s.parse().ok()).unwrap_or(0),
|
downloaded: row.get(5).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
last_updated: row.get(6).and_then(|s| s.parse().ok()).unwrap_or(0),
|
interval: row.get(6).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
interval: row.get(7).and_then(|s| s.parse().ok()).unwrap_or(0),
|
last_updated: 0,
|
||||||
status: "Unknown".to_string(), // Can derive from message or activity later, or keep unknown
|
status: "Unknown".to_string(), // Can derive from activity later, or keep unknown
|
||||||
message: row.get(8).cloned().unwrap_or_default(),
|
message: "".to_string(),
|
||||||
})
|
})
|
||||||
.collect())
|
.collect();
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(SetFilePriority, "/api/server_fns")]
|
#[server(SetFilePriority, "/api/server_fns")]
|
||||||
|
|||||||
26
test_rpc.rs
Normal file
26
test_rpc.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use shared::xmlrpc::{RtorrentClient, RpcParam, parse_multicall_response};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let client = RtorrentClient::new("/tmp/rtorrent.sock");
|
||||||
|
|
||||||
|
// Hardcode a known hash from the UI, e.g. "C3315ABFAD70C54505813D1303C1457900C5B795" (from first image)
|
||||||
|
let hash = "C3315ABFAD70C54505813D1303C1457900C5B795";
|
||||||
|
|
||||||
|
let params = vec![
|
||||||
|
RpcParam::from(hash),
|
||||||
|
RpcParam::from(""),
|
||||||
|
RpcParam::from("t.url="),
|
||||||
|
];
|
||||||
|
|
||||||
|
match client.call("t.multicall", ¶ms).await {
|
||||||
|
Ok(xml) => {
|
||||||
|
println!("Response XML:\n{}", xml);
|
||||||
|
match parse_multicall_response(&xml) {
|
||||||
|
Ok(rows) => println!("Rows ({})", rows.len()),
|
||||||
|
Err(e) => println!("Parse error: {:?}", e),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => println!("Error: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user