From 626c5290002f535cb93f10f6013e5456c0eea7b0 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 14:24:16 +0000 Subject: [PATCH 01/28] Created upnp crate to port forward local ports --- Cargo.lock | 58 +++ Cargo.toml | 3 +- crates/upnp/Cargo.toml | 22 + crates/upnp/examples/upnp-forward.rs | 24 + crates/upnp/src/lib.rs | 480 +++++++++++++++++++ crates/upnp/src/resources/test/devices-0.xml | 77 +++ 6 files changed, 663 insertions(+), 1 deletion(-) create mode 100644 crates/upnp/Cargo.toml create mode 100644 crates/upnp/examples/upnp-forward.rs create mode 100644 crates/upnp/src/lib.rs create mode 100644 crates/upnp/src/resources/test/devices-0.xml diff --git a/Cargo.lock b/Cargo.lock index 2df7daa..1ec8066 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,6 +95,17 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "async-recursion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -1378,6 +1389,23 @@ dependencies = [ "sha1", ] +[[package]] +name = "librqbit-upnp" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-recursion", + "futures", + "network-interface", + "reqwest", + "serde", + "serde-xml-rs", + "tokio", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "linux-raw-sys" version = "0.4.12" @@ -1471,6 +1499,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "network-interface" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68759ef97fe9c9e46f79ea8736c19f1d28992e24c8dc8ce86752918bfeaae7" +dependencies = [ + "cc", + "libc", + "thiserror", + "winapi", +] + [[package]] name = "nom" version = "7.1.3" @@ -2099,6 +2139,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-xml-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + [[package]] name = "serde_derive" version = "1.0.193" @@ -2994,3 +3046,9 @@ checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ "tap", ] + +[[package]] +name = "xml-rs" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" diff --git a/Cargo.toml b/Cargo.toml index 4ac75b7..166e697 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,8 @@ members = [ "crates/sha1w", "crates/librqbit_core", "crates/peer_binary_protocol", - "crates/dht" + "crates/dht", + "crates/upnp" ] [profile.dev] diff --git a/crates/upnp/Cargo.toml b/crates/upnp/Cargo.toml new file mode 100644 index 0000000..07b5cc1 --- /dev/null +++ b/crates/upnp/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "librqbit-upnp" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tracing = "0.1" +anyhow = "1" +reqwest = {version = "0.11"} +serde = {version = "1", features = ["derive"]} +serde-xml-rs = "0.6.0" +tokio = {version = "1"} +futures = "0.3" +url = "2" +async-recursion = "1" +network-interface = "1" + +[dev-dependencies] +tokio = {version = "1", features = ["macros", "rt-multi-thread"]} +tracing-subscriber = "0.3" \ No newline at end of file diff --git a/crates/upnp/examples/upnp-forward.rs b/crates/upnp/examples/upnp-forward.rs new file mode 100644 index 0000000..d426ad1 --- /dev/null +++ b/crates/upnp/examples/upnp-forward.rs @@ -0,0 +1,24 @@ +use librqbit_upnp::UpnpPortForwarder; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + let args: Vec = std::env::args().collect(); + if args.len() != 2 { + eprintln!("Usage: {} ", args[0]); + return Ok(()); + } + + let port: u16 = match args[1].parse() { + Ok(p) => p, + Err(_) => { + eprintln!("Invalid port number: {}", args[1]); + return Ok(()); + } + }; + + let port_forwarder = UpnpPortForwarder::new(vec![port], None)?; + + port_forwarder.run_forever().await; + Ok(()) +} diff --git a/crates/upnp/src/lib.rs b/crates/upnp/src/lib.rs new file mode 100644 index 0000000..614e1d5 --- /dev/null +++ b/crates/upnp/src/lib.rs @@ -0,0 +1,480 @@ +use anyhow::{bail, Context}; +use futures::{stream::FuturesUnordered, StreamExt, TryFutureExt}; +use network_interface::NetworkInterfaceConfig; +use reqwest::Client; +use serde::Deserialize; +use serde_xml_rs::from_str; +use std::{ + collections::{HashMap, HashSet}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + time::Duration, +}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; +use tracing::{debug, error, error_span, trace, warn, Instrument, Span}; +use url::Url; + +const SERVICE_TYPE_WAN_IP_CONNECTION: &str = "urn:schemas-upnp-org:service:WANIPConnection:1"; +const SSDP_MULTICAST_IP: SocketAddr = + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(239, 255, 255, 250), 1900)); +const SSDP_SEARCH_REQUEST: &str = "M-SEARCH * HTTP/1.1\r\n\ + Host: 239.255.255.250:1900\r\n\ + Man: \"ssdp:discover\"\r\n\ + MX: 3\r\n\ + ST: upnp:rootdevice\r\n\ + \r\n"; + +fn get_local_ip_relative_to(local_dest: Ipv4Addr) -> anyhow::Result { + // Ipv4Addr.to_bits() is only there in nightly rust, so copying here for now. + fn ip_bits(addr: Ipv4Addr) -> u32 { + u32::from_be_bytes(addr.octets()) + } + + fn masked(ip: Ipv4Addr, mask: Ipv4Addr) -> u32 { + ip_bits(ip) & ip_bits(mask) + } + + let interfaces = + network_interface::NetworkInterface::show().context("error listing network interfaces")?; + + for i in interfaces { + for addr in i.addr { + if let network_interface::Addr::V4(v4) = addr { + let ip = v4.ip; + let mask = match v4.netmask { + Some(mask) => mask, + None => continue, + }; + trace!("found local addr {ip}/{mask}"); + // If the masked address is the same, means we are on the same network, return + // the ip address + if masked(ip, mask) == masked(local_dest, mask) { + return Ok(ip); + } + } + } + } + bail!("couldn't find a local ip address") +} + +async fn forward_port( + control_url: Url, + local_ip: Ipv4Addr, + port: u16, + lease_duration: Duration, +) -> anyhow::Result<()> { + let request_body = format!( + r#" + + + + + {} + TCP + {} + {} + 1 + rust UPnP + {} + + + + "#, + SERVICE_TYPE_WAN_IP_CONNECTION, + port, + port, + local_ip, + lease_duration.as_secs() + ); + + let url = control_url; + + let client = reqwest::Client::new(); + let response = client + .post(url.clone()) + .header("Content-Type", "text/xml") + .header( + "SOAPAction", + format!("\"{}#AddPortMapping\"", SERVICE_TYPE_WAN_IP_CONNECTION), + ) + .body(request_body) + .send() + .await + .context("error sending")?; + + let status = response.status(); + + let response_text = response + .text() + .await + .context("error reading response text")?; + + trace!("AddPortMapping response: {} {}", status, response_text); + if !status.is_success() { + bail!("failed port forwarding: {}", status); + } else { + debug!("successfully port forwarded {}:{}", local_ip, port); + } + Ok(()) +} + +#[derive(Clone, Debug, Deserialize)] +struct RootDesc { + #[serde(rename = "device")] + devices: Vec, +} + +#[derive(Default, Clone, Debug, Deserialize)] +pub struct DeviceList { + #[serde(rename = "device")] + devices: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Device { + #[serde(rename = "deviceType")] + pub device_type: String, + #[serde(rename = "friendlyName", default)] + pub friendly_name: String, + #[serde(rename = "serviceList", default)] + pub service_list: ServiceList, + #[serde(rename = "deviceList", default)] + pub device_list: DeviceList, +} + +impl Device { + pub fn iter_services( + &self, + parent: Span, + ) -> Box + '_> { + let self_span = self.span(parent); + let services = self.service_list.services.iter().map({ + let self_span = self_span.clone(); + move |s| (s.span(self_span.clone()), s) + }); + Box::new(services.chain(self.device_list.devices.iter().flat_map({ + let self_span = self_span.clone(); + move |d| d.iter_services(self_span.clone()) + }))) + } + + pub fn span(&self, parent: tracing::Span) -> tracing::Span { + error_span!(parent: parent, "device", name = self.name()) + } +} + +impl Device { + pub fn name(&self) -> &str { + if self.friendly_name.is_empty() { + return &self.device_type; + } + &self.friendly_name + } +} + +#[derive(Clone, Debug, Deserialize, Default)] +pub struct ServiceList { + #[serde(rename = "service", default)] + pub services: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Service { + #[serde(rename = "serviceType")] + pub service_type: String, + #[serde(rename = "controlURL")] + pub control_url: String, + #[serde(rename = "SCPDURL")] + pub scpd_url: String, +} + +impl Service { + pub fn span(&self, parent: tracing::Span) -> tracing::Span { + error_span!(parent: parent, "service", url = self.control_url) + } +} + +#[derive(Debug)] +struct UpnpEndpoint { + discover_response: UpnpDiscoverResponse, + data: RootDesc, +} + +impl UpnpEndpoint { + fn location(&self) -> &Url { + &self.discover_response.location + } + + fn span(&self) -> tracing::Span { + error_span!("upnp_endpoint", location = %self.location()) + } + + fn iter_services(&self) -> impl Iterator + '_ { + let self_span = self.span(); + self.data + .devices + .iter() + .flat_map(move |d| d.iter_services(self_span.clone())) + } + + fn my_local_ip(&self) -> anyhow::Result { + let dest_ipv4 = match self.discover_response.received_from { + SocketAddr::V4(v4) => *v4.ip(), + SocketAddr::V6(v6) => { + bail!("don't support IPv6, but remote ip is {}", v6.ip()) + } + }; + let local_ip = get_local_ip_relative_to(dest_ipv4) + .with_context(|| format!("can't determine local IP relative to {dest_ipv4}"))?; + Ok(local_ip) + } + + fn get_wan_ip_control_urls(&self) -> impl Iterator + '_ { + self.iter_services() + .filter(|(_, s)| s.service_type == SERVICE_TYPE_WAN_IP_CONNECTION) + .map(|(span, s)| (span, self.discover_response.location.join(&s.control_url))) + .filter_map(|(span, url)| match url { + Ok(url) => Some((span, url)), + Err(e) => { + error!("bad control url: {e:#}"); + None + } + }) + } +} + +#[derive(Debug)] +struct UpnpDiscoverResponse { + pub received_from: SocketAddr, + pub location: Url, +} + +async fn discover_services(location: Url) -> anyhow::Result { + let response = Client::new() + .get(location.clone()) + .send() + .await + .context("failed to send GET request")? + .text() + .await + .context("failed to read response body")?; + trace!("received from {location}: {response}"); + let root_desc: RootDesc = from_str(&response) + .context("failed to parse response body as xml") + .map_err(|e| { + error!("failed to parse this XML: {response}"); + e + })?; + Ok(root_desc) +} + +fn parse_upnp_discover_response( + response: &str, + received_from: SocketAddr, +) -> anyhow::Result { + let mut headers = HashMap::new(); + for line in response.lines() { + if let Some((key, value)) = line.split_once(": ") { + headers.insert(key.to_lowercase(), value.trim_end().to_string()); + } + } + let location = headers.get("location").context("missing location header")?; + let location = + Url::parse(location).with_context(|| format!("failed parsing location {location}"))?; + Ok(UpnpDiscoverResponse { + location, + received_from, + }) +} + +pub struct UpnpPortForwarderOptions { + pub lease_duration: Duration, + pub discover_interval: Duration, + pub discover_timeout: Duration, +} + +impl Default for UpnpPortForwarderOptions { + fn default() -> Self { + Self { + discover_interval: Duration::from_secs(60), + discover_timeout: Duration::from_secs(10), + lease_duration: Duration::from_secs(60), + } + } +} + +pub struct UpnpPortForwarder { + ports: Vec, + opts: UpnpPortForwarderOptions, +} + +impl UpnpPortForwarder { + pub fn new(ports: Vec, opts: Option) -> anyhow::Result { + if ports.is_empty() { + bail!("empty ports") + } + Ok(Self { + ports, + opts: opts.unwrap_or_default(), + }) + } + + async fn parse_endpoint( + &self, + discover_response: UpnpDiscoverResponse, + ) -> anyhow::Result { + let services = discover_services(discover_response.location.clone()).await?; + Ok(UpnpEndpoint { + discover_response, + data: services, + }) + } + + async fn discover_once( + &self, + tx: &UnboundedSender, + ) -> anyhow::Result<()> { + let socket = tokio::net::UdpSocket::bind("0.0.0.0:0") + .await + .context("failed to bind UDP socket")?; + socket + .send_to(SSDP_SEARCH_REQUEST.as_bytes(), SSDP_MULTICAST_IP) + .await + .context("failed to send SSDP search request")?; + + let mut buffer = [0; 2048]; + + let timeout = tokio::time::sleep(self.opts.discover_timeout); + let mut timed_out = false; + tokio::pin!(timeout); + + let mut discovered = 0; + + while !timed_out { + tokio::select! { + _ = &mut timeout, if !timed_out => { + timed_out = true; + } + Ok((len, addr)) = socket.recv_from(&mut buffer), if !timed_out => { + let response = match std::str::from_utf8(&buffer[..len]) { + Ok(response) => response, + Err(_) => { + warn!("received invalid utf-8 from {addr}"); + continue; + }, + }; + trace!("received response from {addr}: {response}"); + match parse_upnp_discover_response(response, addr) { + Ok(r) => { + tx.send(r)?; + discovered += 1; + }, + Err(e) => warn!("failed to parse response: {e:#}"), + }; + }, + } + } + + debug!("discovered {discovered} endpoints"); + Ok(()) + } + + async fn discovery(&self, tx: UnboundedSender) -> anyhow::Result<()> { + let mut discover_interval = tokio::time::interval(self.opts.discover_interval); + + loop { + discover_interval.tick().await; + if let Err(e) = self.discover_once(&tx).await { + warn!("failed to run discovery: {e:#}"); + } + } + } + + async fn manage_port(&self, control_url: Url, local_ip: Ipv4Addr, port: u16) -> ! { + let lease_duration = self.opts.lease_duration; + let mut interval = tokio::time::interval(lease_duration / 2); + loop { + interval.tick().await; + if let Err(e) = forward_port(control_url.clone(), local_ip, port, lease_duration).await + { + warn!("failed to forward port: {e:#}"); + } + } + } + + async fn manage_service(&self, control_url: Url, local_ip: Ipv4Addr) -> anyhow::Result<()> { + futures::future::join_all(self.ports.iter().cloned().map(|port| { + self.manage_port(control_url.clone(), local_ip, port) + .instrument(error_span!("manage_port", port = port)) + })) + .await; + Ok(()) + } + + pub async fn run_forever(self) -> ! { + let (discover_tx, mut discover_rx) = unbounded_channel(); + let discovery = self.discovery(discover_tx); + + let mut spawned_tasks = HashSet::::new(); + + let mut endpoints = FuturesUnordered::new(); + let mut service_managers = FuturesUnordered::new(); + + tokio::pin!(discovery); + + loop { + tokio::select! { + _ = &mut discovery => {}, + r = discover_rx.recv() => { + let r = r.unwrap(); + let location = r.location.clone(); + endpoints.push(self.parse_endpoint(r).map_err(|e| { + error!("failed to parse endpoint: {e:#}"); + e + }).instrument(error_span!("parse endpoint", location=location.to_string()))); + }, + Some(Ok(endpoint)) = endpoints.next(), if !endpoints.is_empty() => { + let mut local_ip = None; + for (span, control_url) in endpoint.get_wan_ip_control_urls() { + if spawned_tasks.contains(&control_url) { + debug!("already spawned for {}", control_url); + continue; + } + let ip = match local_ip { + Some(ip) => ip, + None => { + match endpoint.my_local_ip() { + Ok(ip) => { + local_ip = Some(ip); + ip + }, + Err(e) => { + warn!("failed to determine local IP for endpoint at {}: {:#}", endpoint.location(), e); + break; + } + } + } + }; + spawned_tasks.insert(control_url.clone()); + service_managers.push(self.manage_service(control_url, ip).instrument(span)) + } + }, + _ = service_managers.next(), if !service_managers.is_empty() => { + + }, + } + } + } +} + +#[cfg(test)] +mod tests { + use serde_xml_rs::from_str; + + use crate::RootDesc; + + #[test] + fn test_parse() { + dbg!(from_str::(include_str!("resources/test/devices-0.xml")).unwrap()); + } +} diff --git a/crates/upnp/src/resources/test/devices-0.xml b/crates/upnp/src/resources/test/devices-0.xml new file mode 100644 index 0000000..7e30770 --- /dev/null +++ b/crates/upnp/src/resources/test/devices-0.xml @@ -0,0 +1,77 @@ + + + 1 + 0 + + + urn:schemas-upnp-org:device:InternetGatewayDevice:1 + ARRIS TG3492LG + Arris Group, Inc + http://www.arris.com/ + DOCSIS 3.1 Cable Modem Gateway Device + TG3492LG + TG3492LG + http://www.arris.com + ABAP02974423 + uuid:ebf5a0a0-1dd1-11b2-a90f-acf8cc3de6b6 + TG3492LG + + + urn:schemas-upnp-org:service:Layer3Forwarding:1 + urn:upnp-org:serviceId:L3Forwarding1 + /Layer3ForwardingSCPD.xml + /upnp/control/Layer3Forwarding + /upnp/event/Layer3Forwarding + + + + + urn:schemas-upnp-org:device:WANDevice:1 + WANDevice:1 + Arris Group, Inc + http://www.arris.com/ + DOCSIS 3.1 Cable Modem Gateway Device + TG3492LG + TG3492LG + http://www.arris.com + ABAP02974423 + uuid:ebf5a0a0-1dd1-11b2-a92f-acf8cc3de6b6 + TG3492LG + + + urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + urn:upnp-org:serviceId:WANCommonIFC1 + /WANCommonInterfaceConfigSCPD.xml + /upnp/control/WANCommonInterfaceConfig0 + /upnp/event/WANCommonInterfaceConfig0 + + + + + urn:schemas-upnp-org:device:WANConnectionDevice:1 + WANConnectionDevice:1 + Arris Group, Inc + http://www.arris.com/ + DOCSIS 3.1 Cable Modem Gateway Device + TG3492LG + TG3492LG + http://www.arris.com + ABAP02974423 + uuid:ebf5a0a0-1dd1-11b2-a93f-acf8cc3de6b6 + TG3492LG + + + urn:schemas-upnp-org:service:WANIPConnection:1 + urn:upnp-org:serviceId:WANIPConn1 + /WANIPConnectionServiceSCPD.xml + /upnp/control/WANIPConnection0 + /upnp/event/WANIPConnection0 + + + + + + + http://192.168.0.1/ + + \ No newline at end of file From 71d49a88b686e31217bcc9f904ab7168e7affef4 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 14:48:19 +0000 Subject: [PATCH 02/28] Create TCP listener --- Cargo.lock | 1 + crates/librqbit/Cargo.toml | 1 + crates/librqbit/src/session.rs | 132 +++++++++++++++++++++++++++------ 3 files changed, 111 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ec8066..b55d42f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1275,6 +1275,7 @@ dependencies = [ "librqbit-dht", "librqbit-peer-protocol", "librqbit-sha1-wrapper", + "librqbit-upnp", "openssl", "parking_lot", "rand", diff --git a/crates/librqbit/Cargo.toml b/crates/librqbit/Cargo.toml index 25fbe25..a22500d 100644 --- a/crates/librqbit/Cargo.toml +++ b/crates/librqbit/Cargo.toml @@ -29,6 +29,7 @@ clone_to_owned = {path = "../clone_to_owned", package="librqbit-clone-to-owned", peer_binary_protocol = {path = "../peer_binary_protocol", package="librqbit-peer-protocol", version = "3.2.1"} sha1w = {path = "../sha1w", default-features=false, package="librqbit-sha1-wrapper", version="2.2.1"} dht = {path = "../dht", package="librqbit-dht", version="4.0.0"} +librqbit-upnp = {path = "../upnp", version = "0.1.0"} tokio = {version = "1", features = ["macros", "rt-multi-thread"]} axum = {version = "0.7"} diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 42c2166..c46dbd4 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -23,6 +23,7 @@ use parking_lot::RwLock; use reqwest::Url; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_with::serde_as; +use tokio::net::TcpListener; use tracing::{debug, error, error_span, info, warn}; use crate::{ @@ -147,6 +148,9 @@ pub struct Session { spawner: BlockingSpawner, db: RwLock, output_folder: PathBuf, + + cancel_tx: tokio::sync::watch::Sender<()>, + cancel_rx: tokio::sync::watch::Receiver<()>, } async fn torrent_from_url(url: &str) -> anyhow::Result { @@ -322,6 +326,23 @@ pub struct SessionOptions { pub peer_id: Option, /// Configure default peer connection options. Can be overriden per torrent. pub peer_opts: Option, + + pub listen_port_range: Option>, + pub enable_upnp_port_forwarding: bool, +} + +async fn create_tcp_listener( + port_range: std::ops::Range, +) -> anyhow::Result<(TcpListener, u16)> { + for port in port_range.clone() { + match TcpListener::bind(("0.0.0.0", port)).await { + Ok(l) => return Ok((l, port)), + Err(e) => { + debug!("error listening on port {port}: {e:#}") + } + } + } + bail!("no free TCP ports in range {port_range:?}"); } impl Session { @@ -336,6 +357,16 @@ impl Session { opts: SessionOptions, ) -> anyhow::Result> { let peer_id = opts.peer_id.unwrap_or_else(generate_peer_id); + + let (tcp_listener, port) = if let Some(port_range) = opts.listen_port_range { + let (l, p) = create_tcp_listener(port_range) + .await + .context("error listening on TCP")?; + (Some(l), Some(p)) + } else { + (None, None) + }; + let dht = if opts.disable_dht { None } else { @@ -355,6 +386,9 @@ impl Session { .join("session.json"), }; let spawner = BlockingSpawner::default(); + + let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(()); + let session = Arc::new(Self { persistence_filename, peer_id, @@ -363,8 +397,28 @@ impl Session { spawner, output_folder, db: RwLock::new(Default::default()), + cancel_rx, + cancel_tx, }); + if let Some(tcp_listener) = tcp_listener { + session.spawn( + "tcp listener", + error_span!("tcp_listen", port = port), + session.clone().task_tcp_listener(tcp_listener), + ); + } + + if let Some(listen_port) = port { + if opts.enable_upnp_port_forwarding { + session.spawn( + "upnp_forward", + error_span!("upnp_forward", port = listen_port), + session.clone().task_upnp_port_forwarder(listen_port), + ); + } + } + if opts.persistence { info!( "will use {:?} for session persistence", @@ -375,36 +429,50 @@ impl Session { format!("couldn't create directory {:?} for session storage", parent) })?; } - let session = session.clone(); - spawn( + let persistence_task = session.clone().task_persistence(); + session.spawn( "session persistene", error_span!("session_persistence"), - async move { - // Populate initial from the state filename - if let Err(e) = session.populate_from_stored().await { - error!("could not populate session from stored file: {:?}", e); - } - - let session = Arc::downgrade(&session); - - loop { - tokio::time::sleep(Duration::from_secs(10)).await; - let session = match session.upgrade() { - Some(s) => s, - None => break, - }; - if let Err(e) = session.dump_to_disk() { - error!("error dumping session to disk: {:?}", e); - } - } - - Ok(()) - }, + persistence_task, ); } Ok(session) } + + async fn task_persistence(self: Arc) -> anyhow::Result<()> { + // Populate initial from the state filename + if let Err(e) = self.populate_from_stored().await { + error!("could not populate session from stored file: {:?}", e); + } + + let session = Arc::downgrade(&self); + drop(self); + + loop { + tokio::time::sleep(Duration::from_secs(10)).await; + let session = match session.upgrade() { + Some(s) => s, + None => break, + }; + if let Err(e) = session.dump_to_disk() { + error!("error dumping session to disk: {:?}", e); + } + } + + Ok(()) + } + + async fn task_tcp_listener(self: Arc, l: TcpListener) -> anyhow::Result<()> { + // TODO + Ok(()) + } + + async fn task_upnp_port_forwarder(self: Arc, port: u16) -> anyhow::Result<()> { + let pf = librqbit_upnp::UpnpPortForwarder::new(vec![port], None)?; + pf.run_forever().await + } + pub fn get_dht(&self) -> Option<&Dht> { self.dht.as_ref() } @@ -425,6 +493,24 @@ impl Session { } } + fn spawn( + &self, + name: &str, + span: tracing::Span, + fut: impl std::future::Future> + Send + 'static, + ) { + let mut cancel_rx = self.cancel_rx.clone(); + spawn(name, span, async move { + tokio::select! { + r = fut => r, + _ = cancel_rx.changed() => { + debug!("task canceled"); + Ok(()) + } + } + }); + } + async fn populate_from_stored(self: &Arc) -> anyhow::Result<()> { let mut rdr = match std::fs::File::open(&self.persistence_filename) { Ok(f) => BufReader::new(f), From 41fb3bfd37e833d066dc2755cd288a38e5e52550 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 16:35:16 +0000 Subject: [PATCH 03/28] Accepting TCP connections + publishing = works. Still yet to pass it to the live torrent --- crates/dht/src/dht.rs | 5 ++- crates/dht/src/persistence.rs | 5 ++- crates/librqbit/Cargo.toml | 2 +- crates/librqbit/src/session.rs | 69 ++++++++++++++++++++++++++++++---- crates/rqbit/src/main.rs | 23 ++++++++++++ 5 files changed, 93 insertions(+), 11 deletions(-) diff --git a/crates/dht/src/dht.rs b/crates/dht/src/dht.rs index c780f1c..1dbc0b1 100644 --- a/crates/dht/src/dht.rs +++ b/crates/dht/src/dht.rs @@ -1,6 +1,6 @@ use std::{ cmp::Reverse, - net::SocketAddr, + net::{SocketAddr, SocketAddrV4}, sync::{ atomic::{AtomicU16, Ordering}, Arc, @@ -1059,7 +1059,8 @@ pub struct DhtConfig { pub bootstrap_addrs: Option>, pub routing_table: Option, pub listen_addr: Option, - pub(crate) peer_store: Option, + pub announce_addr: Option, + pub peer_store: Option, } impl DhtState { diff --git a/crates/dht/src/persistence.rs b/crates/dht/src/persistence.rs index 49d478e..1e39c05 100644 --- a/crates/dht/src/persistence.rs +++ b/crates/dht/src/persistence.rs @@ -16,10 +16,11 @@ use crate::peer_store::PeerStore; use crate::routing_table::RoutingTable; use crate::{Dht, DhtConfig, DhtState}; -#[derive(Default, Clone)] +#[derive(Default)] pub struct PersistentDhtConfig { pub dump_interval: Option, pub config_filename: Option, + pub announce_addr: Option, } #[derive(Serialize, Deserialize)] @@ -111,11 +112,13 @@ impl PersistentDht { .map(|de| (Some(de.addr), Some(de.table), de.peer_store)) .unwrap_or((None, None, None)); let peer_id = routing_table.as_ref().map(|r| r.id()); + let dht_config = DhtConfig { peer_id, routing_table, listen_addr, peer_store, + announce_addr: config.announce_addr, ..Default::default() }; let dht = DhtState::with_config(dht_config).await?; diff --git a/crates/librqbit/Cargo.toml b/crates/librqbit/Cargo.toml index a22500d..87f940c 100644 --- a/crates/librqbit/Cargo.toml +++ b/crates/librqbit/Cargo.toml @@ -42,7 +42,7 @@ anyhow = "1" itertools = "0.12" http = "1" regex = "1" -reqwest = {version="0.11.22", default-features=false} +reqwest = {version="0.11.22", default-features=false, features = ["json"]} urlencoding = "2" byteorder = "1" bincode = "1" diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index c46dbd4..04a8924 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -2,7 +2,7 @@ use std::{ borrow::Cow, collections::{HashMap, HashSet}, io::{BufReader, BufWriter, Read}, - net::SocketAddr, + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, path::PathBuf, str::FromStr, sync::Arc, @@ -12,7 +12,9 @@ use std::{ use anyhow::{bail, Context}; use bencode::{bencode_serialize_to_writer, BencodeDeserializer}; use buffers::{ByteBufT, ByteString}; -use dht::{Dht, DhtBuilder, Id20, PersistentDht, PersistentDhtConfig, RequestPeersStream}; +use dht::{ + Dht, DhtBuilder, DhtConfig, Id20, PersistentDht, PersistentDhtConfig, RequestPeersStream, +}; use librqbit_core::{ directories::get_configuration_directory, magnet::Magnet, @@ -345,6 +347,34 @@ async fn create_tcp_listener( bail!("no free TCP ports in range {port_range:?}"); } +async fn get_public_announce_addr(port: u16) -> anyhow::Result { + async fn get_ipify() -> anyhow::Result { + #[derive(Deserialize)] + struct Data { + ip: Ipv4Addr, + } + let resp: Data = reqwest::get("https://api.ipify.org?format=json") + .await + .context("error getting public IP address")? + .error_for_status()? + .json() + .await?; + Ok(resp.ip) + } + + async fn get_public_ip() -> anyhow::Result { + get_ipify().await + } + + let ip = get_public_ip() + .await + .context("error getting public IP address")?; + + let addr = SocketAddr::V4(SocketAddrV4::new(ip, port)); + info!("using public IP address {addr} to publish on DHT"); + Ok(addr) +} + impl Session { /// Create a new session. The passed in folder will be used as a default unless overriden per torrent. pub async fn new(output_folder: PathBuf) -> anyhow::Result> { @@ -354,7 +384,7 @@ impl Session { /// Create a new session with options. pub async fn new_with_opts( output_folder: PathBuf, - opts: SessionOptions, + mut opts: SessionOptions, ) -> anyhow::Result> { let peer_id = opts.peer_id.unwrap_or_else(generate_peer_id); @@ -362,6 +392,7 @@ impl Session { let (l, p) = create_tcp_listener(port_range) .await .context("error listening on TCP")?; + info!("Listening on 0.0.0.0:{p} for incoming peer connections"); (Some(l), Some(p)) } else { (None, None) @@ -370,10 +401,25 @@ impl Session { let dht = if opts.disable_dht { None } else { - let dht = if opts.disable_dht_persistence { - DhtBuilder::new().await + let announce_addr = if let Some(port) = port { + Some( + get_public_announce_addr(port) + .await + .context("error getting public announce address")?, + ) } else { - PersistentDht::create(opts.dht_config).await + None + }; + let dht = if opts.disable_dht_persistence { + DhtBuilder::with_config(DhtConfig { + announce_addr, + ..Default::default() + }) + .await + } else { + let mut pdht_config = opts.dht_config.take().unwrap_or_default(); + pdht_config.announce_addr = announce_addr; + PersistentDht::create(Some(pdht_config)).await } .context("error initializing DHT")?; Some(dht) @@ -464,7 +510,12 @@ impl Session { } async fn task_tcp_listener(self: Arc, l: TcpListener) -> anyhow::Result<()> { - // TODO + let mut buf = vec![0u8; 4096]; + + loop { + let (stream, addr) = l.accept().await.context("error accepting")?; + info!("accepted connection from {addr}"); + } Ok(()) } @@ -511,6 +562,10 @@ impl Session { }); } + fn stop(&self) { + let _ = self.cancel_tx.send(()); + } + async fn populate_from_stored(self: &Arc) -> anyhow::Result<()> { let mut rdr = match std::fs::File::open(&self.persistence_filename) { Ok(f) => BufReader::new(f), diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index 056ccd6..85b60ca 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -71,6 +71,22 @@ struct Opts { #[arg(short = 't', long)] worker_threads: Option, + // Enable to listen on 0.0.0.0 on TCP for torrent requests. + #[arg(long = "tcp-listen", default_value = "true")] + tcp_listen: bool, + + /// The minimal port to listen for incoming connections. + #[arg(long = "tcp-min-port", default_value = "4240")] + tcp_listen_min_port: u16, + + /// The maximal port to listen for incoming connections. + #[arg(long = "tcp-max-port", default_value = "4260")] + tcp_listen_max_port: u16, + + /// If set, will try to publish the chosen port through upnp on your router. + #[arg(long = "enable-upnp", default_value = "true")] + enable_upnp: bool, + #[command(subcommand)] subcommand: SubCommand, } @@ -311,6 +327,12 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> { read_write_timeout: Some(opts.peer_read_write_timeout), ..Default::default() }), + listen_port_range: if opts.tcp_listen { + Some(opts.tcp_listen_min_port..opts.tcp_listen_max_port) + } else { + None + }, + enable_upnp_port_forwarding: opts.enable_upnp, }; let stats_printer = |session: Arc| async move { @@ -371,6 +393,7 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> { sopts.persistence = !start_opts.disable_persistence; sopts.persistence_filename = start_opts.persistence_filename.clone().map(PathBuf::from); + let session = Session::new_with_opts(PathBuf::from(&start_opts.output_folder), sopts) .await From 9c7cf61e1ab0ed810f6ddd68033086a591cefd23 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 17:01:30 +0000 Subject: [PATCH 04/28] Handshake clone to owned --- crates/librqbit/src/peer_connection.rs | 13 ++++- crates/librqbit/src/peer_info_reader/mod.rs | 4 +- crates/librqbit/src/torrent_state/live/mod.rs | 6 +-- crates/peer_binary_protocol/src/lib.rs | 54 ++++++++++++++----- 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/crates/librqbit/src/peer_connection.rs b/crates/librqbit/src/peer_connection.rs index a6034f4..ee87eec 100644 --- a/crates/librqbit/src/peer_connection.rs +++ b/crates/librqbit/src/peer_connection.rs @@ -23,7 +23,7 @@ pub trait PeerConnectionHandler { fn on_connected(&self, _connection_time: Duration) {} fn get_have_bytes(&self) -> u64; fn serialize_bitfield_message_to_buf(&self, buf: &mut Vec) -> anyhow::Result; - fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()>; + fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()>; fn on_extended_handshake( &self, extended_handshake: &ExtendedHandshake, @@ -120,7 +120,16 @@ impl PeerConnection { } } - pub async fn manage_peer( + pub async fn manage_peer_incoming( + &self, + mut outgoing_chan: tokio::sync::mpsc::UnboundedReceiver, + handshake: Handshake, + socket: tokio::net::TcpSocket, + ) -> anyhow::Result<()> { + todo!() + } + + pub async fn manage_peer_outgoing( &self, mut outgoing_chan: tokio::sync::mpsc::UnboundedReceiver, ) -> anyhow::Result<()> { diff --git a/crates/librqbit/src/peer_info_reader/mod.rs b/crates/librqbit/src/peer_info_reader/mod.rs index 45ab101..f0a52a5 100644 --- a/crates/librqbit/src/peer_info_reader/mod.rs +++ b/crates/librqbit/src/peer_info_reader/mod.rs @@ -51,7 +51,7 @@ pub(crate) async fn read_metainfo_from_peer( ); let result_reader = async move { result_rx.await? }; - let connection_runner = async move { connection.manage_peer(writer_rx).await }; + let connection_runner = async move { connection.manage_peer_outgoing(writer_rx).await }; tokio::select! { result = result_reader => result, @@ -145,7 +145,7 @@ impl PeerConnectionHandler for Handler { Ok(0) } - fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> { + fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> { if !handshake.supports_extended() { anyhow::bail!("this peer does not support extended handshaking, which is a prerequisite to download metadata") } diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 47eb680..0276a08 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -404,7 +404,7 @@ impl TorrentStateLive { .fetch_add(1, Ordering::Relaxed); let res = tokio::select! { r = requester => {r} - r = peer_connection.manage_peer(rx) => {r} + r = peer_connection.manage_peer_outgoing(rx) => {r} }; handler.state.peer_semaphore.add_permits(1); @@ -502,7 +502,7 @@ impl TorrentStateLive { matches!(self.get_next_needed_piece(handle), Ok(Some(_))) } - fn set_peer_live(&self, handle: PeerHandle, h: Handshake) { + fn set_peer_live(&self, handle: PeerHandle, h: Handshake) { let result = self.peers.with_peer_mut(handle, "set_peer_live", |p| { p.state .connecting_to_live(Id20(h.peer_id), &self.peers.stats) @@ -771,7 +771,7 @@ impl<'a> PeerConnectionHandler for &'a PeerHandler { Ok(len) } - fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> { + fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> { self.state.set_peer_live(self.addr, handshake); Ok(()) } diff --git a/crates/peer_binary_protocol/src/lib.rs b/crates/peer_binary_protocol/src/lib.rs index 2535ac7..f448b87 100644 --- a/crates/peer_binary_protocol/src/lib.rs +++ b/crates/peer_binary_protocol/src/lib.rs @@ -5,7 +5,7 @@ pub mod extended; use bincode::Options; -use buffers::{ByteBuf, ByteString}; +use buffers::{ByteBuf, ByteBufT, ByteString}; use byteorder::{ByteOrder, BE}; use clone_to_owned::CloneToOwned; use librqbit_core::{constants::CHUNK_SIZE, id20::Id20, lengths::ChunkInfo}; @@ -472,8 +472,8 @@ where } #[derive(Serialize, Deserialize, Debug)] -pub struct Handshake<'a> { - pub pstr: &'a str, +pub struct Handshake { + pub pstr: ByteBuf, pub reserved: [u8; 8], pub info_hash: [u8; 20], pub peer_id: [u8; 20], @@ -485,8 +485,8 @@ fn bopts() -> impl bincode::Options { .with_big_endian() } -impl<'a> Handshake<'a> { - pub fn new(info_hash: Id20, peer_id: Id20) -> Handshake<'static> { +impl Handshake> { + pub fn new(info_hash: Id20, peer_id: Id20) -> Handshake> { debug_assert_eq!(PSTR_BT1.len(), 19); let mut reserved: u64 = 0; @@ -496,19 +496,16 @@ impl<'a> Handshake<'a> { BE::write_u64(&mut reserved_arr, reserved); Handshake { - pstr: PSTR_BT1, + pstr: ByteBuf(PSTR_BT1.as_bytes()), reserved: reserved_arr, info_hash: info_hash.0, peer_id: peer_id.0, } } - pub fn supports_extended(&self) -> bool { - self.reserved[5] & 0x10 > 0 - } - fn bopts() -> impl bincode::Options { - bincode::DefaultOptions::new() - } - pub fn deserialize(b: &[u8]) -> Result<(Handshake<'_>, usize), MessageDeserializeError> { + + pub fn deserialize( + b: &[u8], + ) -> Result<(Handshake>, usize), MessageDeserializeError> { let pstr_len = *b .first() .ok_or(MessageDeserializeError::NotEnoughData(1, "handshake"))?; @@ -526,11 +523,40 @@ impl<'a> Handshake<'a> { expected_len, )) } - pub fn serialize(&self, buf: &mut Vec) { +} + +impl Handshake { + pub fn supports_extended(&self) -> bool { + self.reserved[5] & 0x10 > 0 + } + fn bopts() -> impl bincode::Options { + bincode::DefaultOptions::new() + } + + pub fn serialize(&self, buf: &mut Vec) + where + B: Serialize, + { Self::bopts().serialize_into(buf, &self).unwrap() } } +impl CloneToOwned for Handshake +where + B: CloneToOwned, +{ + type Target = Handshake<::Target>; + + fn clone_to_owned(&self) -> Self::Target { + Handshake { + pstr: self.pstr.clone_to_owned(), + reserved: self.reserved, + info_hash: self.info_hash, + peer_id: self.peer_id, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub struct Request { pub index: u32, From 65c69f576b45f5a26cdf3c1579af5779efc03544 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 18:24:49 +0000 Subject: [PATCH 05/28] Code fully compiles for processing incoming peers --- Cargo.lock | 8 +- crates/dht/Cargo.toml | 2 +- crates/dht/src/dht.rs | 2 +- crates/librqbit/Cargo.toml | 4 +- crates/librqbit/src/peer_connection.rs | 86 ++++++++++-- crates/librqbit/src/session.rs | 123 ++++++++++++++++-- crates/librqbit/src/torrent_state/live/mod.rs | 100 +++++++++++++- .../src/torrent_state/live/peer/mod.rs | 9 ++ crates/peer_binary_protocol/Cargo.toml | 2 +- crates/peer_binary_protocol/src/lib.rs | 2 +- crates/rqbit/Cargo.toml | 4 +- 11 files changed, 310 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b55d42f..be8d3ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1253,7 +1253,7 @@ dependencies = [ [[package]] name = "librqbit" -version = "4.0.0" +version = "4.1.0" dependencies = [ "anyhow", "axum 0.7.1", @@ -1343,7 +1343,7 @@ dependencies = [ [[package]] name = "librqbit-dht" -version = "4.0.0" +version = "4.1.0" dependencies = [ "anyhow", "backoff", @@ -1368,7 +1368,7 @@ dependencies = [ [[package]] name = "librqbit-peer-protocol" -version = "3.2.1" +version = "3.3.0" dependencies = [ "anyhow", "bincode", @@ -2002,7 +2002,7 @@ dependencies = [ [[package]] name = "rqbit" -version = "4.0.0" +version = "4.1.0" dependencies = [ "anyhow", "clap", diff --git a/crates/dht/Cargo.toml b/crates/dht/Cargo.toml index 41418d1..f5da4e5 100644 --- a/crates/dht/Cargo.toml +++ b/crates/dht/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librqbit-dht" -version = "4.0.0" +version = "4.1.0" edition = "2021" description = "DHT implementation, used in rqbit torrent client." license = "Apache-2.0" diff --git a/crates/dht/src/dht.rs b/crates/dht/src/dht.rs index 1dbc0b1..3581bfe 100644 --- a/crates/dht/src/dht.rs +++ b/crates/dht/src/dht.rs @@ -1,6 +1,6 @@ use std::{ cmp::Reverse, - net::{SocketAddr, SocketAddrV4}, + net::SocketAddr, sync::{ atomic::{AtomicU16, Ordering}, Arc, diff --git a/crates/librqbit/Cargo.toml b/crates/librqbit/Cargo.toml index 87f940c..3f4afa8 100644 --- a/crates/librqbit/Cargo.toml +++ b/crates/librqbit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librqbit" -version = "4.0.0" +version = "4.1.0" authors = ["Igor Katson "] edition = "2021" description = "The main library used by rqbit torrent client. The binary is just a small wrapper on top of it." @@ -26,7 +26,7 @@ bencode = {path = "../bencode", default-features=false, package="librqbit-bencod buffers = {path = "../buffers", package="librqbit-buffers", version = "2.2.1"} librqbit-core = {path = "../librqbit_core", version = "3.2.1"} clone_to_owned = {path = "../clone_to_owned", package="librqbit-clone-to-owned", version = "2.2.1"} -peer_binary_protocol = {path = "../peer_binary_protocol", package="librqbit-peer-protocol", version = "3.2.1"} +peer_binary_protocol = {path = "../peer_binary_protocol", package="librqbit-peer-protocol", version = "3.3.0"} sha1w = {path = "../sha1w", default-features=false, package="librqbit-sha1-wrapper", version="2.2.1"} dht = {path = "../dht", package="librqbit-dht", version="4.0.0"} librqbit-upnp = {path = "../upnp", version = "0.1.0"} diff --git a/crates/librqbit/src/peer_connection.rs b/crates/librqbit/src/peer_connection.rs index ee87eec..c2e2841 100644 --- a/crates/librqbit/src/peer_connection.rs +++ b/crates/librqbit/src/peer_connection.rs @@ -62,7 +62,7 @@ pub(crate) struct PeerConnection { spawner: BlockingSpawner, } -async fn with_timeout( +pub(crate) async fn with_timeout( timeout_value: Duration, fut: impl std::future::Future>, ) -> anyhow::Result @@ -120,18 +120,57 @@ impl PeerConnection { } } + // By the time this is called: + // read_buf should start with valuable data. The handshake should be removed from it. pub async fn manage_peer_incoming( &self, - mut outgoing_chan: tokio::sync::mpsc::UnboundedReceiver, + outgoing_chan: tokio::sync::mpsc::UnboundedReceiver, + // How many bytes into read buffer have we read already. + read_so_far: usize, + read_buf: Vec, handshake: Handshake, - socket: tokio::net::TcpSocket, + mut conn: tokio::net::TcpStream, ) -> anyhow::Result<()> { - todo!() + use tokio::io::AsyncWriteExt; + + let rwtimeout = self + .options + .read_write_timeout + .unwrap_or_else(|| Duration::from_secs(10)); + + if handshake.info_hash != self.info_hash.0 { + anyhow::bail!("wrong info hash"); + } + + trace!( + "incoming connection: id={:?}", + try_decode_peer_id(Id20(handshake.peer_id)) + ); + + let mut write_buf = Vec::::with_capacity(PIECE_MESSAGE_DEFAULT_LEN); + let handshake = Handshake::new(self.info_hash, self.peer_id); + handshake.serialize(&mut write_buf); + with_timeout(rwtimeout, conn.write_all(&write_buf)) + .await + .context("error writing handshake")?; + write_buf.clear(); + + let h_supports_extended = handshake.supports_extended(); + + self.manage_peer( + h_supports_extended, + read_so_far, + read_buf, + write_buf, + conn, + outgoing_chan, + ) + .await } pub async fn manage_peer_outgoing( &self, - mut outgoing_chan: tokio::sync::mpsc::UnboundedReceiver, + outgoing_chan: tokio::sync::mpsc::UnboundedReceiver, ) -> anyhow::Result<()> { use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; @@ -170,20 +209,51 @@ impl PeerConnection { let (h, size) = Handshake::deserialize(&read_buf[..read_so_far]) .map_err(|e| anyhow::anyhow!("error deserializing handshake: {:?}", e))?; + let h_supports_extended = h.supports_extended(); trace!("connected: id={:?}", try_decode_peer_id(Id20(h.peer_id))); if h.info_hash != self.info_hash.0 { anyhow::bail!("info hash does not match"); } - let mut extended_handshake: Option> = None; - let supports_extended = h.supports_extended(); - self.handler.on_handshake(h)?; + if read_so_far > size { read_buf.copy_within(size..read_so_far, 0); } read_so_far -= size; + self.manage_peer( + h_supports_extended, + read_so_far, + read_buf, + write_buf, + conn, + outgoing_chan, + ) + .await + } + + async fn manage_peer( + &self, + handshake_supports_extended: bool, + // How many bytes into read_buf is there of peer-sent-data. + mut read_so_far: usize, + mut read_buf: Vec, + mut write_buf: Vec, + mut conn: tokio::net::TcpStream, + mut outgoing_chan: tokio::sync::mpsc::UnboundedReceiver, + ) -> anyhow::Result<()> { + use tokio::io::AsyncReadExt; + use tokio::io::AsyncWriteExt; + + let rwtimeout = self + .options + .read_write_timeout + .unwrap_or_else(|| Duration::from_secs(10)); + + let mut extended_handshake: Option> = None; + let supports_extended = handshake_supports_extended; + if supports_extended { let my_extended = Message::Extended(ExtendedMessage::Handshake(ExtendedHandshake::new())); diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 04a8924..5f695db 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -12,9 +12,11 @@ use std::{ use anyhow::{bail, Context}; use bencode::{bencode_serialize_to_writer, BencodeDeserializer}; use buffers::{ByteBufT, ByteString}; +use clone_to_owned::CloneToOwned; use dht::{ Dht, DhtBuilder, DhtConfig, Id20, PersistentDht, PersistentDhtConfig, RequestPeersStream, }; +use futures::{stream::FuturesUnordered, StreamExt, TryFutureExt}; use librqbit_core::{ directories::get_configuration_directory, magnet::Magnet, @@ -22,17 +24,23 @@ use librqbit_core::{ torrent_metainfo::{torrent_from_bytes, TorrentMetaV1Info, TorrentMetaV1Owned}, }; use parking_lot::RwLock; +use peer_binary_protocol::{Handshake, PIECE_MESSAGE_DEFAULT_LEN}; use reqwest::Url; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_with::serde_as; -use tokio::net::TcpListener; -use tracing::{debug, error, error_span, info, warn}; +use tokio::{ + io::AsyncReadExt, + net::{TcpListener, TcpStream}, +}; +use tracing::{debug, error, error_span, info, trace, warn, Instrument}; use crate::{ dht_utils::{read_metainfo_from_peer_receiver, ReadMetainfoResult}, - peer_connection::PeerConnectionOptions, + peer_connection::{with_timeout, PeerConnectionOptions}, spawn_utils::{spawn, BlockingSpawner}, - torrent_state::{ManagedTorrentBuilder, ManagedTorrentHandle, ManagedTorrentState}, + torrent_state::{ + ManagedTorrentBuilder, ManagedTorrentHandle, ManagedTorrentState, TorrentStateLive, + }, }; pub const SUPPORTED_SCHEMES: [&str; 3] = ["http:", "https:", "magnet:"]; @@ -375,6 +383,14 @@ async fn get_public_announce_addr(port: u16) -> anyhow::Result { Ok(addr) } +pub(crate) struct CheckedIncomingConnection { + pub addr: SocketAddr, + pub stream: tokio::net::TcpStream, + pub read_buf: Vec, + pub handshake: Handshake, + pub read_so_far: usize, +} + impl Session { /// Create a new session. The passed in folder will be used as a default unless overriden per torrent. pub async fn new(output_folder: PathBuf) -> anyhow::Result> { @@ -509,14 +525,103 @@ impl Session { Ok(()) } + async fn check_incoming_connection( + &self, + addr: SocketAddr, + mut stream: TcpStream, + ) -> anyhow::Result<(Arc, CheckedIncomingConnection)> { + // TODO: move buffer handling to peer_connection + + let rwtimeout = self + .peer_opts + .read_write_timeout + .unwrap_or_else(|| Duration::from_secs(10)); + + let mut read_buf = vec![0u8; PIECE_MESSAGE_DEFAULT_LEN * 2]; + let mut read_so_far = with_timeout(rwtimeout, stream.read(&mut read_buf)) + .await + .context("error reading handshake")?; + if read_so_far == 0 { + anyhow::bail!("bad handshake"); + } + let (h, size) = Handshake::deserialize(&read_buf[..read_so_far]) + .map_err(|e| anyhow::anyhow!("error deserializing handshake: {:?}", e))?; + + trace!("received handshake from {addr}: {:?}", h); + + for (id, torrent) in self.db.read().torrents.iter() { + if torrent.info_hash().0 != h.info_hash { + continue; + } + + let live = match torrent.live() { + Some(live) => live, + None => { + bail!("torrent {id} is not live, ignoring connection"); + } + }; + + let handshake = h.clone_to_owned(); + + if read_so_far > size { + read_buf.copy_within(size..read_so_far, 0); + } + read_so_far -= size; + + return Ok(( + live, + CheckedIncomingConnection { + addr, + stream, + handshake, + read_buf, + read_so_far, + }, + )); + } + + bail!("didn't find a matching torrent for {:?}", h.info_hash) + } + + fn handover_checked_connection( + &self, + live: Arc, + checked: CheckedIncomingConnection, + ) -> anyhow::Result<()> { + live.add_incoming_peer(checked) + } + async fn task_tcp_listener(self: Arc, l: TcpListener) -> anyhow::Result<()> { - let mut buf = vec![0u8; 4096]; + let mut futs = FuturesUnordered::new(); loop { - let (stream, addr) = l.accept().await.context("error accepting")?; - info!("accepted connection from {addr}"); + tokio::select! { + r = l.accept() => { + match r { + Ok((stream, addr)) => { + trace!("accepted connection from {addr}"); + futs.push( + self.check_incoming_connection(addr, stream) + .map_err(|e| { + error!("error checking incoming connection: {e:#}"); + e + }) + .instrument(error_span!("incoming", addr=%addr)) + ); + } + Err(e) => { + error!("error accepting: {e:#}"); + continue; + } + } + }, + Some(Ok((live, checked))) = futs.next(), if !futs.is_empty() => { + if let Err(e) = self.handover_checked_connection(live, checked) { + warn!("error handing over incoming connection: {e:#}"); + } + }, + } } - Ok(()) } async fn task_upnp_port_forwarder(self: Arc, port: u16) -> anyhow::Result<()> { @@ -562,7 +667,7 @@ impl Session { }); } - fn stop(&self) { + pub fn stop(&self) { let _ = self.cancel_tx.send(()); } diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 0276a08..a39f035 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -89,7 +89,9 @@ use crate::{ peer_connection::{ PeerConnection, PeerConnectionHandler, PeerConnectionOptions, WriterRequest, }, + session::CheckedIncomingConnection, spawn_utils::spawn, + torrent_state::peer::Peer, tracker_comms::{TrackerError, TrackerRequest, TrackerRequestEvent, TrackerResponse}, type_aliases::{PeerHandle, BF}, }; @@ -100,7 +102,7 @@ use self::{ atomic::PeerCountersAtomic as AtomicPeerCounters, snapshot::{PeerStatsFilter, PeerStatsSnapshot}, }, - InflightRequest, PeerState, PeerTx, SendMany, + InflightRequest, PeerRx, PeerState, PeerTx, SendMany, }, peers::PeerStates, stats::{atomic::AtomicStats, snapshot::StatsSnapshot}, @@ -361,7 +363,99 @@ impl TorrentStateLive { } } - async fn task_manage_peer(self: Arc, addr: SocketAddr) -> anyhow::Result<()> { + pub(crate) fn add_incoming_peer( + self: &Arc, + checked_peer: CheckedIncomingConnection, + ) -> anyhow::Result<()> { + use dashmap::mapref::entry::Entry; + let (tx, rx) = unbounded_channel(); + + let counters = match self.peers.states.entry(checked_peer.addr) { + Entry::Occupied(_) => bail!("we are already managing peer {}", checked_peer.addr), + Entry::Vacant(vac) => { + let peer = Peer::new_live_for_incoming_connection( + Id20(checked_peer.handshake.peer_id), + tx.clone(), + ); + let counters = peer.stats.counters.clone(); + vac.insert(peer); + counters + } + }; + + self.spawn( + "incoming peer", + error_span!("manage_incoming_peer", addr = %checked_peer.addr), + self.clone() + .task_manage_incoming_peer(checked_peer, counters, tx, rx), + ); + Ok(()) + } + + async fn task_manage_incoming_peer( + self: Arc, + checked_peer: CheckedIncomingConnection, + counters: Arc, + tx: PeerTx, + rx: PeerRx, + ) -> anyhow::Result<()> { + // TODO: bump counters for incoming + + let handler = PeerHandler { + addr: checked_peer.addr, + on_bitfield_notify: Default::default(), + unchoke_notify: Default::default(), + locked: RwLock::new(PeerHandlerLocked { + i_am_choked: true, + previously_requested_pieces: BF::new(), + }), + requests_sem: Semaphore::new(0), + state: self.clone(), + tx, + counters, + }; + let options = PeerConnectionOptions { + connect_timeout: self.meta.options.peer_connect_timeout, + read_write_timeout: self.meta.options.peer_read_write_timeout, + ..Default::default() + }; + let peer_connection = PeerConnection::new( + checked_peer.addr, + self.meta.info_hash, + self.meta.peer_id, + &handler, + Some(options), + self.meta.spawner, + ); + let requester = handler.task_peer_chunk_requester(checked_peer.addr); + + let res = tokio::select! { + r = requester => {r} + r = peer_connection.manage_peer_incoming( + rx, + checked_peer.read_so_far, + checked_peer.read_buf, + checked_peer.handshake, + checked_peer.stream + ) => {r} + }; + + handler.state.peer_semaphore.add_permits(1); + + match res { + // We disconnected the peer ourselves as we don't need it + Ok(()) => { + handler.on_peer_died(None)?; + } + Err(e) => { + debug!("error managing peer: {:#}", e); + handler.on_peer_died(Some(e))?; + } + }; + Ok(()) + } + + async fn task_manage_outgoing_peer(self: Arc, addr: SocketAddr) -> anyhow::Result<()> { let state = self; let (rx, tx) = state.peers.mark_peer_connecting(addr)?; @@ -440,7 +534,7 @@ impl TorrentStateLive { state.spawn( "manage_peer", error_span!(parent: state.meta.span.clone(), "manage_peer", peer = addr.to_string()), - state.clone().task_manage_peer(addr), + state.clone().task_manage_outgoing_peer(addr), ); } } diff --git a/crates/librqbit/src/torrent_state/live/peer/mod.rs b/crates/librqbit/src/torrent_state/live/peer/mod.rs index 675d762..b0eee03 100644 --- a/crates/librqbit/src/torrent_state/live/peer/mod.rs +++ b/crates/librqbit/src/torrent_state/live/peer/mod.rs @@ -52,6 +52,15 @@ pub(crate) struct Peer { pub stats: stats::atomic::PeerStats, } +impl Peer { + pub fn new_live_for_incoming_connection(peer_id: Id20, tx: PeerTx) -> Self { + Self { + state: PeerStateNoMut(PeerState::Live(LivePeerState::new(peer_id, tx))), + stats: Default::default(), + } + } +} + #[derive(Debug, Default)] pub(crate) enum PeerState { #[default] diff --git a/crates/peer_binary_protocol/Cargo.toml b/crates/peer_binary_protocol/Cargo.toml index 03dc948..8261b54 100644 --- a/crates/peer_binary_protocol/Cargo.toml +++ b/crates/peer_binary_protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librqbit-peer-protocol" -version = "3.2.1" +version = "3.3.0" edition = "2021" description = "Protocol for working with torrent peers. Used in rqbit torrent client." license = "Apache-2.0" diff --git a/crates/peer_binary_protocol/src/lib.rs b/crates/peer_binary_protocol/src/lib.rs index f448b87..11171f7 100644 --- a/crates/peer_binary_protocol/src/lib.rs +++ b/crates/peer_binary_protocol/src/lib.rs @@ -5,7 +5,7 @@ pub mod extended; use bincode::Options; -use buffers::{ByteBuf, ByteBufT, ByteString}; +use buffers::{ByteBuf, ByteString}; use byteorder::{ByteOrder, BE}; use clone_to_owned::CloneToOwned; use librqbit_core::{constants::CHUNK_SIZE, id20::Id20, lengths::ChunkInfo}; diff --git a/crates/rqbit/Cargo.toml b/crates/rqbit/Cargo.toml index 1a68457..741a1da 100644 --- a/crates/rqbit/Cargo.toml +++ b/crates/rqbit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rqbit" -version = "4.0.0" +version = "4.1.0" authors = ["Igor Katson "] edition = "2021" description = "A bittorrent command line client and server." @@ -23,7 +23,7 @@ default-tls = ["librqbit/default-tls"] rust-tls = ["librqbit/rust-tls"] [dependencies] -librqbit = {path="../librqbit", default-features=false, version = "4.0.0"} +librqbit = {path="../librqbit", default-features=false, version = "4.1.0"} tokio = {version = "1", features = ["macros", "rt-multi-thread"]} console-subscriber = {version = "0.2", optional = true} anyhow = "1" From b15815d12f2cad72cf5c47851d9e80e07cc90ece Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 19:07:18 +0000 Subject: [PATCH 06/28] Adding options to test downloading from another instance --- crates/librqbit/src/session.rs | 31 ++++++++++++++++++------------- crates/rqbit/src/main.rs | 33 +++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 5f695db..8d7eba2 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -228,6 +228,8 @@ pub struct AddTorrentOptions { #[serde_as(as = "Option")] pub force_tracker_interval: Option, + pub disable_trackers: bool, + /// Initial peers to start of with. pub initial_peers: Option>, @@ -549,6 +551,10 @@ impl Session { trace!("received handshake from {addr}: {:?}", h); + if h.peer_id == self.peer_id.0 { + bail!("seems like we are connecting to ourselves, ignoring"); + } + for (id, torrent) in self.db.read().torrents.iter() { if torrent.info_hash().0 != h.info_hash { continue; @@ -580,15 +586,7 @@ impl Session { )); } - bail!("didn't find a matching torrent for {:?}", h.info_hash) - } - - fn handover_checked_connection( - &self, - live: Arc, - checked: CheckedIncomingConnection, - ) -> anyhow::Result<()> { - live.add_incoming_peer(checked) + bail!("didn't find a matching torrent for {:?}", Id20(h.info_hash)) } async fn task_tcp_listener(self: Arc, l: TcpListener) -> anyhow::Result<()> { @@ -616,7 +614,7 @@ impl Session { } }, Some(Ok((live, checked))) = futs.next(), if !futs.is_empty() => { - if let Err(e) = self.handover_checked_connection(live, checked) { + if let Err(e) = live.add_incoming_peer(checked) { warn!("error handing over incoming connection: {e:#}"); } }, @@ -881,7 +879,11 @@ impl Session { torrent.info, dht_rx, trackers, - Default::default(), + opts.initial_peers + .clone() + .unwrap_or_default() + .into_iter() + .collect(), ) } }; @@ -989,8 +991,11 @@ impl Session { builder .overwrite(opts.overwrite) .spawner(self.spawner) - .peer_id(self.peer_id) - .trackers(trackers); + .peer_id(self.peer_id); + + if opts.disable_trackers { + builder.trackers(trackers); + } if let Some(only_files) = only_files { builder.only_files(only_files); diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index 85b60ca..e015708 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -72,8 +72,8 @@ struct Opts { worker_threads: Option, // Enable to listen on 0.0.0.0 on TCP for torrent requests. - #[arg(long = "tcp-listen", default_value = "true")] - tcp_listen: bool, + #[arg(long = "disable-tcp-listen")] + disable_tcp_listen: bool, /// The minimal port to listen for incoming connections. #[arg(long = "tcp-min-port", default_value = "4240")] @@ -84,8 +84,8 @@ struct Opts { tcp_listen_max_port: u16, /// If set, will try to publish the chosen port through upnp on your router. - #[arg(long = "enable-upnp", default_value = "true")] - enable_upnp: bool, + #[arg(long = "disable-upnp")] + disable_upnp: bool, #[command(subcommand)] subcommand: SubCommand, @@ -148,6 +148,25 @@ struct DownloadOpts { /// Exit the program once the torrents complete download. #[arg(short = 'e', long)] exit_on_finish: bool, + + #[arg(long = "disable-trackers")] + disable_trackers: bool, + + #[arg(long = "initial-peers")] + initial_peers: Option, +} + +#[derive(Clone)] +struct InitialPeers(Vec); + +impl From<&str> for InitialPeers { + fn from(s: &str) -> Self { + let mut v = Vec::new(); + for addr in s.split(',') { + v.push(addr.parse().unwrap()); + } + Self(v) + } } // server start @@ -327,12 +346,12 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> { read_write_timeout: Some(opts.peer_read_write_timeout), ..Default::default() }), - listen_port_range: if opts.tcp_listen { + listen_port_range: if !opts.disable_tcp_listen { Some(opts.tcp_listen_min_port..opts.tcp_listen_max_port) } else { None }, - enable_upnp_port_forwarding: opts.enable_upnp, + enable_upnp_port_forwarding: !opts.disable_upnp, }; let stats_printer = |session: Arc| async move { @@ -424,6 +443,8 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> { force_tracker_interval: opts.force_tracker_interval, output_folder: download_opts.output_folder.clone(), sub_folder: download_opts.sub_folder.clone(), + initial_peers: download_opts.initial_peers.clone().map(|p| p.0), + disable_trackers: download_opts.disable_trackers, ..Default::default() }; let connect_to_existing = match client.validate_rqbit_server().await { From efaa36a16150f47cdcce080886eb037665774c34 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 20:02:54 +0000 Subject: [PATCH 07/28] SAving, its broken --- crates/librqbit/src/peer_connection.rs | 2 + crates/librqbit/src/torrent_state/live/mod.rs | 86 ++++++++++--------- .../src/torrent_state/live/peer/mod.rs | 17 +++- .../src/torrent_state/live/peers/mod.rs | 5 ++ 4 files changed, 67 insertions(+), 43 deletions(-) diff --git a/crates/librqbit/src/peer_connection.rs b/crates/librqbit/src/peer_connection.rs index c2e2841..33c3f2a 100644 --- a/crates/librqbit/src/peer_connection.rs +++ b/crates/librqbit/src/peer_connection.rs @@ -157,6 +157,8 @@ impl PeerConnection { let h_supports_extended = handshake.supports_extended(); + self.handler.on_handshake(handshake)?; + self.manage_peer( h_supports_extended, read_so_far, diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index a39f035..c10ab16 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -376,6 +376,7 @@ impl TorrentStateLive { let peer = Peer::new_live_for_incoming_connection( Id20(checked_peer.handshake.peer_id), tx.clone(), + &self.peers.stats, ); let counters = peer.stats.counters.clone(); vac.insert(peer); @@ -400,7 +401,6 @@ impl TorrentStateLive { rx: PeerRx, ) -> anyhow::Result<()> { // TODO: bump counters for incoming - let handler = PeerHandler { addr: checked_peer.addr, on_bitfield_notify: Default::default(), @@ -427,7 +427,7 @@ impl TorrentStateLive { Some(options), self.meta.spawner, ); - let requester = handler.task_peer_chunk_requester(checked_peer.addr); + let requester = handler.task_peer_chunk_requester(); let res = tokio::select! { r = requester => {r} @@ -458,7 +458,6 @@ impl TorrentStateLive { async fn task_manage_outgoing_peer(self: Arc, addr: SocketAddr) -> anyhow::Result<()> { let state = self; let (rx, tx) = state.peers.mark_peer_connecting(addr)?; - let counters = state .peers .with_peer(addr, |p| p.stats.counters.clone()) @@ -490,7 +489,7 @@ impl TorrentStateLive { Some(options), state.meta.spawner, ); - let requester = handler.task_peer_chunk_requester(addr); + let requester = handler.task_peer_chunk_requester(); handler .counters @@ -597,18 +596,10 @@ impl TorrentStateLive { } fn set_peer_live(&self, handle: PeerHandle, h: Handshake) { - let result = self.peers.with_peer_mut(handle, "set_peer_live", |p| { + self.peers.with_peer_mut(handle, "set_peer_live", |p| { p.state - .connecting_to_live(Id20(h.peer_id), &self.peers.stats) - .is_some() + .connecting_to_live(Id20(h.peer_id), &self.peers.stats); }); - match result { - Some(true) => { - trace!("set peer to live") - } - Some(false) => debug!("can't set peer live, it was in wrong state"), - None => debug!("can't set peer live, it disappeared"), - } } pub fn get_uploaded_bytes(&self) -> u64 { @@ -867,6 +858,8 @@ impl<'a> PeerConnectionHandler for &'a PeerHandler { fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> { self.state.set_peer_live(self.addr, handshake); + self.tx + .send(WriterRequest::Message(MessageOwned::Unchoke))?; Ok(()) } @@ -1116,7 +1109,8 @@ impl PeerHandler { self.state .peers .with_live_mut(self.addr, "on_have", |live| { - // If bitfield wasn't allocated yet, let's do it. Some clients send haves before bitfield. + // If bitfield wasn't allocated yet, let's do it. Some clients start empty so they never + // send bitfields. if live.bitfield.is_empty() { live.bitfield = BF::from_vec(vec![0; self.state.lengths.piece_bitfield_bytes()]); @@ -1129,6 +1123,7 @@ impl PeerHandler { } }; trace!("updated bitfield with have={}", have); + self.on_bitfield_notify.notify_waiters(); }); } @@ -1144,10 +1139,40 @@ impl PeerHandler { self.state .peers .update_bitfield_from_vec(self.addr, bitfield.0); + self.on_bitfield_notify.notify_waiters(); + Ok(()) + } + + async fn wait_for_any_notify(&self, notify: &Notify, check: impl Fn() -> bool) { + // To remove possibility of races, we first grab a token, then check + // if we need it, and only if so, await. + let notified = notify.notified(); + if check() { + return; + } + notified.await; + } + + async fn wait_for_bitfield(&self) { + self.wait_for_any_notify(&self.on_bitfield_notify, || { + self.state + .peers + .with_live(self.addr, |live| !live.bitfield.is_empty()) + .unwrap_or_default() + }) + .await; + } + + async fn wait_for_unchoke(&self) { + self.wait_for_any_notify(&self.unchoke_notify, || !self.locked.read().i_am_choked) + .await; + } + + async fn task_peer_chunk_requester(&self) -> anyhow::Result<()> { + let handle = self.addr; + self.wait_for_bitfield().await; if !self.state.am_i_interested_in_peer(self.addr) { - self.tx - .send(WriterRequest::Message(MessageOwned::Unchoke))?; self.tx .send(WriterRequest::Message(MessageOwned::NotInterested))?; if self.state.is_finished() { @@ -1155,32 +1180,11 @@ impl PeerHandler { } return Ok(()); } - - self.on_bitfield_notify.notify_waiters(); - Ok(()) - } - - async fn task_peer_chunk_requester(&self, handle: PeerHandle) -> anyhow::Result<()> { - self.on_bitfield_notify.notified().await; - self.tx.send_many([ - WriterRequest::Message(MessageOwned::Unchoke), - WriterRequest::Message(MessageOwned::Interested), - ])?; - - #[allow(unused_must_use)] - { - timeout(Duration::from_secs(60), self.unchoke_notify.notified()).await; - } + self.tx + .send_many([WriterRequest::Message(MessageOwned::Interested)])?; loop { - if self.locked.read().i_am_choked { - debug!("we are choked, can't reserve next piece"); - #[allow(unused_must_use)] - { - timeout(Duration::from_secs(60), self.unchoke_notify.notified()).await; - } - continue; - } + self.wait_for_unchoke().await; if self.state.is_finished() { debug!("nothing left to download, looping forever until manage_peer quits"); diff --git a/crates/librqbit/src/torrent_state/live/peer/mod.rs b/crates/librqbit/src/torrent_state/live/peer/mod.rs index b0eee03..75d182b 100644 --- a/crates/librqbit/src/torrent_state/live/peer/mod.rs +++ b/crates/librqbit/src/torrent_state/live/peer/mod.rs @@ -53,9 +53,15 @@ pub(crate) struct Peer { } impl Peer { - pub fn new_live_for_incoming_connection(peer_id: Id20, tx: PeerTx) -> Self { + pub fn new_live_for_incoming_connection( + peer_id: Id20, + tx: PeerTx, + counters: &AggregatePeerStatsAtomic, + ) -> Self { + let state = PeerStateNoMut(PeerState::Live(LivePeerState::new(peer_id, tx))); + counters.inc(&state.0); Self { - state: PeerStateNoMut(PeerState::Live(LivePeerState::new(peer_id, tx))), + state, stats: Default::default(), } } @@ -118,6 +124,13 @@ impl PeerStateNoMut { std::mem::replace(&mut self.0, new) } + pub fn get_live(&self) -> Option<&LivePeerState> { + match &self.0 { + PeerState::Live(l) => Some(l), + _ => None, + } + } + pub fn get_live_mut(&mut self) -> Option<&mut LivePeerState> { match &mut self.0 { PeerState::Live(l) => Some(l), diff --git a/crates/librqbit/src/torrent_state/live/peers/mod.rs b/crates/librqbit/src/torrent_state/live/peers/mod.rs index df359b8..2e4b4af 100644 --- a/crates/librqbit/src/torrent_state/live/peers/mod.rs +++ b/crates/librqbit/src/torrent_state/live/peers/mod.rs @@ -53,6 +53,11 @@ impl PeerStates { .map(|e| f(TimedExistence::new(e, reason).value_mut())) } + pub fn with_live(&self, addr: PeerHandle, f: impl FnOnce(&LivePeerState) -> R) -> Option { + self.with_peer(addr, |peer| peer.state.get_live().map(f)) + .flatten() + } + pub fn with_live_mut( &self, addr: PeerHandle, From 4784f3f14b14d3a2a58b7a6aa3b09f11b0e21cba Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 20:31:06 +0000 Subject: [PATCH 08/28] Uploading seems to work fine now --- crates/librqbit/src/torrent_state/live/mod.rs | 45 +++++++------------ .../src/torrent_state/live/peer/mod.rs | 16 ------- crates/rqbit/src/main.rs | 1 + 3 files changed, 17 insertions(+), 45 deletions(-) diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index c10ab16..db03efe 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -102,7 +102,7 @@ use self::{ atomic::PeerCountersAtomic as AtomicPeerCounters, snapshot::{PeerStatsFilter, PeerStatsSnapshot}, }, - InflightRequest, PeerRx, PeerState, PeerTx, SendMany, + InflightRequest, PeerRx, PeerState, PeerTx, }, peers::PeerStates, stats::{atomic::AtomicStats, snapshot::StatsSnapshot}, @@ -571,30 +571,6 @@ impl TorrentStateLive { TimedExistence::new(timeit(reason, || self.locked.write()), reason) } - fn get_next_needed_piece( - &self, - peer_handle: PeerHandle, - ) -> anyhow::Result> { - self.peers - .with_live_mut(peer_handle, "l(get_next_needed_piece)", |live| { - let g = self.lock_read("g(get_next_needed_piece)"); - let bf = &live.bitfield; - for n in g.get_chunks()?.iter_needed_pieces() { - if bf.get(n).map(|v| *v) == Some(true) { - // in theory it should be safe without validation, but whatever. - return Ok(self.lengths.validate_piece_index(n as u32)); - } - } - Ok(None) - }) - .transpose() - .map(|r| r.flatten()) - } - - fn am_i_interested_in_peer(&self, handle: PeerHandle) -> bool { - matches!(self.get_next_needed_piece(handle), Ok(Some(_))) - } - fn set_peer_live(&self, handle: PeerHandle, h: Handshake) { self.peers.with_peer_mut(handle, "set_peer_live", |p| { p.state @@ -1172,16 +1148,27 @@ impl PeerHandler { let handle = self.addr; self.wait_for_bitfield().await; - if !self.state.am_i_interested_in_peer(self.addr) { + // TODO: this check needs to happen more often + if self.state.is_finished() { self.tx .send(WriterRequest::Message(MessageOwned::NotInterested))?; - if self.state.is_finished() { + + if self + .state + .peers + .with_live(self.addr, |l| { + l.has_full_torrent(self.state.lengths.total_pieces() as usize) + }) + .unwrap_or_default() + { + debug!("both peer and us have full torrent, disconnecting"); self.tx.send(WriterRequest::Disconnect)?; + return Ok(()); } - return Ok(()); } + self.tx - .send_many([WriterRequest::Message(MessageOwned::Interested)])?; + .send(WriterRequest::Message(MessageOwned::Interested))?; loop { self.wait_for_unchoke().await; diff --git a/crates/librqbit/src/torrent_state/live/peer/mod.rs b/crates/librqbit/src/torrent_state/live/peer/mod.rs index 75d182b..37cfce4 100644 --- a/crates/librqbit/src/torrent_state/live/peer/mod.rs +++ b/crates/librqbit/src/torrent_state/live/peer/mod.rs @@ -2,8 +2,6 @@ pub mod stats; use std::collections::HashSet; -use anyhow::Context; - use librqbit_core::id20::Id20; use librqbit_core::lengths::{ChunkInfo, ValidPieceIndex}; @@ -29,23 +27,9 @@ impl From<&ChunkInfo> for InflightRequest { } } -// TODO: Arc can be removed probably, as UnboundedSender should be clone + it can be downgraded to weak. pub(crate) type PeerRx = UnboundedReceiver; pub(crate) type PeerTx = UnboundedSender; -pub trait SendMany { - fn send_many(&self, requests: impl IntoIterator) -> anyhow::Result<()>; -} - -impl SendMany for PeerTx { - fn send_many(&self, requests: impl IntoIterator) -> anyhow::Result<()> { - requests - .into_iter() - .try_for_each(|r| self.send(r)) - .context("peer dropped") - } -} - #[derive(Debug, Default)] pub(crate) struct Peer { pub state: PeerStateNoMut, diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index e015708..502a180 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -338,6 +338,7 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> { disable_dht: opts.disable_dht, disable_dht_persistence: opts.disable_dht_persistence, dht_config: None, + // This will be overriden by "server start" below if needed. persistence: false, persistence_filename: None, peer_id: None, From 80df2c10013ce1bf5dc27cb656f15fd42a77c382 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 20:52:30 +0000 Subject: [PATCH 09/28] Display upload speed in Web UI --- crates/librqbit/src/torrent_state/live/mod.rs | 25 ++++++++---- crates/librqbit/src/torrent_state/stats.rs | 14 +++++-- crates/librqbit/webui/dist/assets/index.js | 18 ++++----- crates/librqbit/webui/dist/manifest.json | 2 +- crates/librqbit/webui/src/api-types.ts | 11 ++++-- crates/librqbit/webui/src/rqbit-web.tsx | 38 ++++++++++++------- crates/librqbit_core/src/speed_estimator.rs | 32 +++++++++------- crates/rqbit/src/main.rs | 4 +- 8 files changed, 89 insertions(+), 55 deletions(-) diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index db03efe..41083d6 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -188,7 +188,8 @@ pub struct TorrentStateLive { cancel_tx: tokio::sync::watch::Sender<()>, cancel_rx: tokio::sync::watch::Receiver<()>, - speed_estimator: SpeedEstimator, + down_speed_estimator: SpeedEstimator, + up_speed_estimator: SpeedEstimator, } impl TorrentStateLive { @@ -198,7 +199,8 @@ impl TorrentStateLive { ) -> Arc { let (peer_queue_tx, peer_queue_rx) = unbounded_channel(); - let speed_estimator = SpeedEstimator::new(5); + let down_speed_estimator = SpeedEstimator::new(5); + let up_speed_estimator = SpeedEstimator::new(5); let have_bytes = paused.have_bytes; let needed_bytes = paused.info.lengths.total_length() - have_bytes; @@ -225,7 +227,8 @@ impl TorrentStateLive { peer_semaphore: Semaphore::new(128), peer_queue_tx, finished_notify: Notify::new(), - speed_estimator, + down_speed_estimator, + up_speed_estimator, cancel_rx, cancel_tx, }); @@ -249,6 +252,7 @@ impl TorrentStateLive { Some(state) => state, None => return Ok(()), }; + let now = Instant::now(); let stats = state.stats_snapshot(); let fetched = stats.fetched_bytes; let needed = state.initially_needed(); @@ -257,8 +261,11 @@ impl TorrentStateLive { .wrapping_sub(fetched) .min(needed - stats.downloaded_and_checked_bytes); state - .speed_estimator - .add_snapshot(fetched, remaining, Instant::now()); + .down_speed_estimator + .add_snapshot(fetched, Some(remaining), now); + state + .up_speed_estimator + .add_snapshot(stats.uploaded_bytes, None, now); tokio::time::sleep(Duration::from_secs(1)).await; } } @@ -291,8 +298,12 @@ impl TorrentStateLive { }); } - pub fn speed_estimator(&self) -> &SpeedEstimator { - &self.speed_estimator + pub fn down_speed_estimator(&self) -> &SpeedEstimator { + &self.down_speed_estimator + } + + pub fn up_speed_estimator(&self) -> &SpeedEstimator { + &self.up_speed_estimator } async fn tracker_one_request(&self, tracker_url: Url) -> anyhow::Result { diff --git a/crates/librqbit/src/torrent_state/stats.rs b/crates/librqbit/src/torrent_state/stats.rs index 2d68d8d..222dba9 100644 --- a/crates/librqbit/src/torrent_state/stats.rs +++ b/crates/librqbit/src/torrent_state/stats.rs @@ -10,6 +10,7 @@ pub struct LiveStats { pub snapshot: StatsSnapshot, pub average_piece_download_time: Option, pub download_speed: Speed, + pub upload_speed: Speed, pub time_remaining: Option, } @@ -17,8 +18,9 @@ impl std::fmt::Display for LiveStats { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "down speed: {}", self.download_speed)?; if let Some(time_remaining) = &self.time_remaining { - write!(f, " eta: {time_remaining}")?; + write!(f, ", eta: {time_remaining}")?; } + write!(f, ", up speed: {}", self.upload_speed)?; Ok(()) } } @@ -26,13 +28,17 @@ impl std::fmt::Display for LiveStats { impl From<&TorrentStateLive> for LiveStats { fn from(live: &TorrentStateLive) -> Self { let snapshot = live.stats_snapshot(); - let estimator = live.speed_estimator(); + let down_estimator = live.down_speed_estimator(); + let up_estimator = live.up_speed_estimator(); Self { average_piece_download_time: snapshot.average_piece_download_time(), snapshot, - download_speed: estimator.download_mbps().into(), - time_remaining: estimator.time_remaining().map(DurationWithHumanReadable), + download_speed: down_estimator.mbps().into(), + upload_speed: up_estimator.mbps().into(), + time_remaining: down_estimator + .time_remaining() + .map(DurationWithHumanReadable), } } } diff --git a/crates/librqbit/webui/dist/assets/index.js b/crates/librqbit/webui/dist/assets/index.js index fd9b77a..18ce718 100644 --- a/crates/librqbit/webui/dist/assets/index.js +++ b/crates/librqbit/webui/dist/assets/index.js @@ -1,4 +1,4 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerPolicy&&(o.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?o.credentials="include":l.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}})();function Yl(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Sa={exports:{}},Xl={},xa={exports:{}},F={};/** +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerPolicy&&(o.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?o.credentials="include":l.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}})();function Yl(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var wa={exports:{}},Xl={},Sa={exports:{}},F={};/** * @license React * react.production.min.js * @@ -6,7 +6,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var zr=Symbol.for("react.element"),Wd=Symbol.for("react.portal"),Vd=Symbol.for("react.fragment"),Qd=Symbol.for("react.strict_mode"),Kd=Symbol.for("react.profiler"),Gd=Symbol.for("react.provider"),Yd=Symbol.for("react.context"),Xd=Symbol.for("react.forward_ref"),Zd=Symbol.for("react.suspense"),Jd=Symbol.for("react.memo"),qd=Symbol.for("react.lazy"),Ju=Symbol.iterator;function bd(e){return e===null||typeof e!="object"?null:(e=Ju&&e[Ju]||e["@@iterator"],typeof e=="function"?e:null)}var ka={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Ea=Object.assign,Ca={};function Bn(e,t,n){this.props=e,this.context=t,this.refs=Ca,this.updater=n||ka}Bn.prototype.isReactComponent={};Bn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Bn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Na(){}Na.prototype=Bn.prototype;function Yi(e,t,n){this.props=e,this.context=t,this.refs=Ca,this.updater=n||ka}var Xi=Yi.prototype=new Na;Xi.constructor=Yi;Ea(Xi,Bn.prototype);Xi.isPureReactComponent=!0;var qu=Array.isArray,Ta=Object.prototype.hasOwnProperty,Zi={current:null},_a={key:!0,ref:!0,__self:!0,__source:!0};function ja(e,t,n){var r,l={},o=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(o=""+t.key),t)Ta.call(t,r)&&!_a.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,A=C[D];if(0>>1;Dl(et,O))Oel(mt,et)?(C[D]=mt,C[Oe]=O,D=Oe):(C[D]=et,C[Le]=O,D=Le);else if(Oel(mt,O))C[D]=mt,C[Oe]=O,D=Oe;else break e}}return L}function l(C,L){var O=C.sortIndex-L.sortIndex;return O!==0?O:C.id-L.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var i=Date,u=i.now();e.unstable_now=function(){return i.now()-u}}var s=[],a=[],m=1,h=null,f=3,w=!1,g=!1,x=!1,R=typeof setTimeout=="function"?setTimeout:null,p=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function v(C){for(var L=n(a);L!==null;){if(L.callback===null)r(a);else if(L.startTime<=C)r(a),L.sortIndex=L.expirationTime,t(s,L);else break;L=n(a)}}function S(C){if(x=!1,v(C),!g)if(n(s)!==null)g=!0,Re(T);else{var L=n(a);L!==null&&Ye(S,L.startTime-C)}}function T(C,L){g=!1,x&&(x=!1,p(j),j=-1),w=!0;var O=f;try{for(v(L),h=n(s);h!==null&&(!(h.expirationTime>L)||C&&!ie());){var D=h.callback;if(typeof D=="function"){h.callback=null,f=h.priorityLevel;var A=D(h.expirationTime<=L);L=e.unstable_now(),typeof A=="function"?h.callback=A:h===n(s)&&r(s),v(L)}else r(s);h=n(s)}if(h!==null)var fe=!0;else{var Le=n(a);Le!==null&&Ye(S,Le.startTime-L),fe=!1}return fe}finally{h=null,f=O,w=!1}}var E=!1,N=null,j=-1,U=5,P=-1;function ie(){return!(e.unstable_now()-PC||125D?(C.sortIndex=O,t(a,C),n(s)===null&&C===n(a)&&(x?(p(j),j=-1):x=!0,Ye(S,O-D))):(C.sortIndex=A,t(s,C),g||w||(g=!0,Re(T))),C},e.unstable_shouldYield=ie,e.unstable_wrapCallback=function(C){var L=f;return function(){var O=f;f=L;try{return C.apply(this,arguments)}finally{f=O}}}})(Pa);Oa.exports=Pa;var cp=Oa.exports;/** + */(function(e){function t(C,L){var O=C.length;C.push(L);e:for(;0>>1,A=C[I];if(0>>1;Il(et,O))Oel(mt,et)?(C[I]=mt,C[Oe]=O,I=Oe):(C[I]=et,C[Le]=O,I=Le);else if(Oel(mt,O))C[I]=mt,C[Oe]=O,I=Oe;else break e}}return L}function l(C,L){var O=C.sortIndex-L.sortIndex;return O!==0?O:C.id-L.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var i=Date,u=i.now();e.unstable_now=function(){return i.now()-u}}var s=[],a=[],m=1,h=null,d=3,w=!1,g=!1,k=!1,R=typeof setTimeout=="function"?setTimeout:null,p=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function v(C){for(var L=n(a);L!==null;){if(L.callback===null)r(a);else if(L.startTime<=C)r(a),L.sortIndex=L.expirationTime,t(s,L);else break;L=n(a)}}function S(C){if(k=!1,v(C),!g)if(n(s)!==null)g=!0,Re(T);else{var L=n(a);L!==null&&Ye(S,L.startTime-C)}}function T(C,L){g=!1,k&&(k=!1,p(j),j=-1),w=!0;var O=d;try{for(v(L),h=n(s);h!==null&&(!(h.expirationTime>L)||C&&!ie());){var I=h.callback;if(typeof I=="function"){h.callback=null,d=h.priorityLevel;var A=I(h.expirationTime<=L);L=e.unstable_now(),typeof A=="function"?h.callback=A:h===n(s)&&r(s),v(L)}else r(s);h=n(s)}if(h!==null)var fe=!0;else{var Le=n(a);Le!==null&&Ye(S,Le.startTime-L),fe=!1}return fe}finally{h=null,d=O,w=!1}}var E=!1,N=null,j=-1,U=5,P=-1;function ie(){return!(e.unstable_now()-PC||125I?(C.sortIndex=O,t(a,C),n(s)===null&&C===n(a)&&(k?(p(j),j=-1):k=!0,Ye(S,O-I))):(C.sortIndex=A,t(s,C),g||w||(g=!0,Re(T))),C},e.unstable_shouldYield=ie,e.unstable_wrapCallback=function(C){var L=d;return function(){var O=d;d=L;try{return C.apply(this,arguments)}finally{d=O}}}})(Oa);La.exports=Oa;var cp=La.exports;/** * @license React * react-dom.production.min.js * @@ -30,15 +30,15 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Fa=y,Te=cp;function k(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Xo=Object.prototype.hasOwnProperty,fp=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,es={},ts={};function dp(e){return Xo.call(ts,e)?!0:Xo.call(es,e)?!1:fp.test(e)?ts[e]=!0:(es[e]=!0,!1)}function pp(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function mp(e,t,n,r){if(t===null||typeof t>"u"||pp(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function ve(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var oe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){oe[e]=new ve(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];oe[t]=new ve(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){oe[e]=new ve(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){oe[e]=new ve(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){oe[e]=new ve(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){oe[e]=new ve(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){oe[e]=new ve(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){oe[e]=new ve(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){oe[e]=new ve(e,5,!1,e.toLowerCase(),null,!1,!1)});var qi=/[\-:]([a-z])/g;function bi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(qi,bi);oe[t]=new ve(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(qi,bi);oe[t]=new ve(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(qi,bi);oe[t]=new ve(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){oe[e]=new ve(e,1,!1,e.toLowerCase(),null,!1,!1)});oe.xlinkHref=new ve("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){oe[e]=new ve(e,1,!1,e.toLowerCase(),null,!0,!0)});function eu(e,t,n,r){var l=oe.hasOwnProperty(t)?oe[t]:null;(l!==null?l.type!==0:r||!(2"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Xo=Object.prototype.hasOwnProperty,fp=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,es={},ts={};function dp(e){return Xo.call(ts,e)?!0:Xo.call(es,e)?!1:fp.test(e)?ts[e]=!0:(es[e]=!0,!1)}function pp(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function mp(e,t,n,r){if(t===null||typeof t>"u"||pp(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function ve(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var oe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){oe[e]=new ve(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];oe[t]=new ve(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){oe[e]=new ve(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){oe[e]=new ve(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){oe[e]=new ve(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){oe[e]=new ve(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){oe[e]=new ve(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){oe[e]=new ve(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){oe[e]=new ve(e,5,!1,e.toLowerCase(),null,!1,!1)});var qi=/[\-:]([a-z])/g;function bi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(qi,bi);oe[t]=new ve(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(qi,bi);oe[t]=new ve(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(qi,bi);oe[t]=new ve(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){oe[e]=new ve(e,1,!1,e.toLowerCase(),null,!1,!1)});oe.xlinkHref=new ve("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){oe[e]=new ve(e,1,!1,e.toLowerCase(),null,!0,!0)});function eu(e,t,n,r){var l=oe.hasOwnProperty(t)?oe[t]:null;(l!==null?l.type!==0:r||!(2u||l[i]!==o[u]){var s=` -`+l[i].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=u);break}}}finally{go=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?rr(e):""}function hp(e){switch(e.tag){case 5:return rr(e.type);case 16:return rr("Lazy");case 13:return rr("Suspense");case 19:return rr("SuspenseList");case 0:case 2:case 15:return e=wo(e.type,!1),e;case 11:return e=wo(e.type.render,!1),e;case 1:return e=wo(e.type,!0),e;default:return""}}function bo(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case hn:return"Fragment";case mn:return"Portal";case Zo:return"Profiler";case tu:return"StrictMode";case Jo:return"Suspense";case qo:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case $a:return(e.displayName||"Context")+".Consumer";case za:return(e._context.displayName||"Context")+".Provider";case nu:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ru:return t=e.displayName||null,t!==null?t:bo(e.type)||"Memo";case gt:t=e._payload,e=e._init;try{return bo(e(t))}catch{}}return null}function vp(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return bo(t);case 8:return t===tu?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Ft(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Ia(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function yp(e){var t=Ia(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Qr(e){e._valueTracker||(e._valueTracker=yp(e))}function Aa(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Ia(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function xl(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ei(e,t){var n=t.checked;return X({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function rs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Ft(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Ua(e,t){t=t.checked,t!=null&&eu(e,"checked",t,!1)}function ti(e,t){Ua(e,t);var n=Ft(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ni(e,t.type,n):t.hasOwnProperty("defaultValue")&&ni(e,t.type,Ft(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ls(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ni(e,t,n){(t!=="number"||xl(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var lr=Array.isArray;function _n(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=Kr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function gr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var sr={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},gp=["Webkit","ms","Moz","O"];Object.keys(sr).forEach(function(e){gp.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),sr[t]=sr[e]})});function Va(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||sr.hasOwnProperty(e)&&sr[e]?(""+t).trim():t+"px"}function Qa(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Va(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var wp=X({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function oi(e,t){if(t){if(wp[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(k(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(k(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(k(61))}if(t.style!=null&&typeof t.style!="object")throw Error(k(62))}}function ii(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ui=null;function lu(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var si=null,jn=null,Rn=null;function us(e){if(e=Ir(e)){if(typeof si!="function")throw Error(k(280));var t=e.stateNode;t&&(t=eo(t),si(e.stateNode,e.type,t))}}function Ka(e){jn?Rn?Rn.push(e):Rn=[e]:jn=e}function Ga(){if(jn){var e=jn,t=Rn;if(Rn=jn=null,us(e),t)for(e=0;e>>=0,e===0?32:31-(Lp(e)/Op|0)|0}var Gr=64,Yr=4194304;function or(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Nl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=or(u):(o&=i,o!==0&&(r=or(o)))}else i=n&~l,i!==0?r=or(i):o!==0&&(r=or(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function $r(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-We(t),e[t]=n}function zp(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=cr),vs=String.fromCharCode(32),ys=!1;function pc(e,t){switch(e){case"keyup":return am.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function mc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var vn=!1;function fm(e,t){switch(e){case"compositionend":return mc(t);case"keypress":return t.which!==32?null:(ys=!0,vs);case"textInput":return e=t.data,e===vs&&ys?null:e;default:return null}}function dm(e,t){if(vn)return e==="compositionend"||!du&&pc(e,t)?(e=fc(),fl=au=Et=null,vn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=xs(n)}}function gc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?gc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function wc(){for(var e=window,t=xl();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=xl(e.document)}return t}function pu(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function xm(e){var t=wc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&gc(n.ownerDocument.documentElement,n)){if(r!==null&&pu(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=ks(n,o);var i=ks(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,yn=null,mi=null,dr=null,hi=!1;function Es(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;hi||yn==null||yn!==xl(r)||(r=yn,"selectionStart"in r&&pu(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),dr&&Cr(dr,r)||(dr=r,r=jl(mi,"onSelect"),0Sn||(e.current=xi[Sn],xi[Sn]=null,Sn--)}function B(e,t){Sn++,xi[Sn]=e.current,e.current=t}var Mt={},ce=$t(Mt),Se=$t(!1),Zt=Mt;function Mn(e,t){var n=e.type.contextTypes;if(!n)return Mt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function xe(e){return e=e.childContextTypes,e!=null}function Ll(){V(Se),V(ce)}function Ls(e,t,n){if(ce.current!==Mt)throw Error(k(168));B(ce,t),B(Se,n)}function jc(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(k(108,vp(e)||"Unknown",l));return X({},n,r)}function Ol(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Mt,Zt=ce.current,B(ce,e),B(Se,Se.current),!0}function Os(e,t,n){var r=e.stateNode;if(!r)throw Error(k(169));n?(e=jc(e,t,Zt),r.__reactInternalMemoizedMergedChildContext=e,V(Se),V(ce),B(ce,e)):V(Se),B(Se,n)}var nt=null,to=!1,Fo=!1;function Rc(e){nt===null?nt=[e]:nt.push(e)}function Fm(e){to=!0,Rc(e)}function Dt(){if(!Fo&&nt!==null){Fo=!0;var e=0,t=I;try{var n=nt;for(I=1;e>=i,l-=i,rt=1<<32-We(t)+l|n<j?(U=N,N=null):U=N.sibling;var P=f(p,N,v[j],S);if(P===null){N===null&&(N=U);break}e&&N&&P.alternate===null&&t(p,N),c=o(P,c,j),E===null?T=P:E.sibling=P,E=P,N=U}if(j===v.length)return n(p,N),Q&&At(p,j),T;if(N===null){for(;jj?(U=N,N=null):U=N.sibling;var ie=f(p,N,P.value,S);if(ie===null){N===null&&(N=U);break}e&&N&&ie.alternate===null&&t(p,N),c=o(ie,c,j),E===null?T=ie:E.sibling=ie,E=ie,N=U}if(P.done)return n(p,N),Q&&At(p,j),T;if(N===null){for(;!P.done;j++,P=v.next())P=h(p,P.value,S),P!==null&&(c=o(P,c,j),E===null?T=P:E.sibling=P,E=P);return Q&&At(p,j),T}for(N=r(p,N);!P.done;j++,P=v.next())P=w(N,p,j,P.value,S),P!==null&&(e&&P.alternate!==null&&N.delete(P.key===null?j:P.key),c=o(P,c,j),E===null?T=P:E.sibling=P,E=P);return e&&N.forEach(function(Ke){return t(p,Ke)}),Q&&At(p,j),T}function R(p,c,v,S){if(typeof v=="object"&&v!==null&&v.type===hn&&v.key===null&&(v=v.props.children),typeof v=="object"&&v!==null){switch(v.$$typeof){case Vr:e:{for(var T=v.key,E=c;E!==null;){if(E.key===T){if(T=v.type,T===hn){if(E.tag===7){n(p,E.sibling),c=l(E,v.props.children),c.return=p,p=c;break e}}else if(E.elementType===T||typeof T=="object"&&T!==null&&T.$$typeof===gt&&Is(T)===E.type){n(p,E.sibling),c=l(E,v.props),c.ref=er(p,E,v),c.return=p,p=c;break e}n(p,E);break}else t(p,E);E=E.sibling}v.type===hn?(c=Yt(v.props.children,p.mode,S,v.key),c.return=p,p=c):(S=wl(v.type,v.key,v.props,null,p.mode,S),S.ref=er(p,c,v),S.return=p,p=S)}return i(p);case mn:e:{for(E=v.key;c!==null;){if(c.key===E)if(c.tag===4&&c.stateNode.containerInfo===v.containerInfo&&c.stateNode.implementation===v.implementation){n(p,c.sibling),c=l(c,v.children||[]),c.return=p,p=c;break e}else{n(p,c);break}else t(p,c);c=c.sibling}c=Bo(v,p.mode,S),c.return=p,p=c}return i(p);case gt:return E=v._init,R(p,c,E(v._payload),S)}if(lr(v))return g(p,c,v,S);if(Xn(v))return x(p,c,v,S);tl(p,v)}return typeof v=="string"&&v!==""||typeof v=="number"?(v=""+v,c!==null&&c.tag===6?(n(p,c.sibling),c=l(c,v),c.return=p,p=c):(n(p,c),c=Uo(v,p.mode,S),c.return=p,p=c),i(p)):n(p,c)}return R}var $n=Dc(!0),Ic=Dc(!1),Ar={},be=$t(Ar),jr=$t(Ar),Rr=$t(Ar);function Kt(e){if(e===Ar)throw Error(k(174));return e}function ku(e,t){switch(B(Rr,t),B(jr,e),B(be,Ar),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:li(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=li(t,e)}V(be),B(be,t)}function Dn(){V(be),V(jr),V(Rr)}function Ac(e){Kt(Rr.current);var t=Kt(be.current),n=li(t,e.type);t!==n&&(B(jr,e),B(be,n))}function Eu(e){jr.current===e&&(V(be),V(jr))}var G=$t(0);function Dl(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if(t.flags&128)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var Mo=[];function Cu(){for(var e=0;en?n:4,e(!0);var r=zo.transition;zo.transition={};try{e(!1),t()}finally{I=n,zo.transition=r}}function tf(){return Ie().memoizedState}function Dm(e,t,n){var r=Ot(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},nf(e))rf(t,n);else if(n=Fc(e,t,n,r),n!==null){var l=me();Ve(n,e,r,l),lf(n,t,r)}}function Im(e,t,n){var r=Ot(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(nf(e))rf(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,Qe(u,i)){var s=t.interleaved;s===null?(l.next=l,Su(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=Fc(e,t,l,r),n!==null&&(l=me(),Ve(n,e,r,l),lf(n,t,r))}}function nf(e){var t=e.alternate;return e===Y||t!==null&&t===Y}function rf(e,t){pr=Il=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function lf(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,iu(e,n)}}var Al={readContext:De,useCallback:ue,useContext:ue,useEffect:ue,useImperativeHandle:ue,useInsertionEffect:ue,useLayoutEffect:ue,useMemo:ue,useReducer:ue,useRef:ue,useState:ue,useDebugValue:ue,useDeferredValue:ue,useTransition:ue,useMutableSource:ue,useSyncExternalStore:ue,useId:ue,unstable_isNewReconciler:!1},Am={readContext:De,useCallback:function(e,t){return Ze().memoizedState=[e,t===void 0?null:t],e},useContext:De,useEffect:Us,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,hl(4194308,4,Zc.bind(null,t,e),n)},useLayoutEffect:function(e,t){return hl(4194308,4,e,t)},useInsertionEffect:function(e,t){return hl(4,2,e,t)},useMemo:function(e,t){var n=Ze();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ze();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Dm.bind(null,Y,e),[r.memoizedState,e]},useRef:function(e){var t=Ze();return e={current:e},t.memoizedState=e},useState:As,useDebugValue:Ru,useDeferredValue:function(e){return Ze().memoizedState=e},useTransition:function(){var e=As(!1),t=e[0];return e=$m.bind(null,e[1]),Ze().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Y,l=Ze();if(Q){if(n===void 0)throw Error(k(407));n=n()}else{if(n=t(),ne===null)throw Error(k(349));qt&30||Hc(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Us(Vc.bind(null,r,o,e),[e]),r.flags|=2048,Pr(9,Wc.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ze(),t=ne.identifierPrefix;if(Q){var n=lt,r=rt;n=(r&~(1<<32-We(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Lr++,0")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=u);break}}}finally{go=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?rr(e):""}function hp(e){switch(e.tag){case 5:return rr(e.type);case 16:return rr("Lazy");case 13:return rr("Suspense");case 19:return rr("SuspenseList");case 0:case 2:case 15:return e=wo(e.type,!1),e;case 11:return e=wo(e.type.render,!1),e;case 1:return e=wo(e.type,!0),e;default:return""}}function bo(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case hn:return"Fragment";case mn:return"Portal";case Zo:return"Profiler";case tu:return"StrictMode";case Jo:return"Suspense";case qo:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case za:return(e.displayName||"Context")+".Consumer";case Ma:return(e._context.displayName||"Context")+".Provider";case nu:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ru:return t=e.displayName||null,t!==null?t:bo(e.type)||"Memo";case gt:t=e._payload,e=e._init;try{return bo(e(t))}catch{}}return null}function vp(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return bo(t);case 8:return t===tu?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Ft(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Ia(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function yp(e){var t=Ia(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Qr(e){e._valueTracker||(e._valueTracker=yp(e))}function Da(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Ia(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Sl(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ei(e,t){var n=t.checked;return X({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function rs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Ft(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Aa(e,t){t=t.checked,t!=null&&eu(e,"checked",t,!1)}function ti(e,t){Aa(e,t);var n=Ft(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ni(e,t.type,n):t.hasOwnProperty("defaultValue")&&ni(e,t.type,Ft(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ls(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ni(e,t,n){(t!=="number"||Sl(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var lr=Array.isArray;function _n(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=Kr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function gr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var sr={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},gp=["Webkit","ms","Moz","O"];Object.keys(sr).forEach(function(e){gp.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),sr[t]=sr[e]})});function Wa(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||sr.hasOwnProperty(e)&&sr[e]?(""+t).trim():t+"px"}function Va(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Wa(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var wp=X({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function oi(e,t){if(t){if(wp[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(x(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(x(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(x(61))}if(t.style!=null&&typeof t.style!="object")throw Error(x(62))}}function ii(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ui=null;function lu(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var si=null,jn=null,Rn=null;function us(e){if(e=Dr(e)){if(typeof si!="function")throw Error(x(280));var t=e.stateNode;t&&(t=eo(t),si(e.stateNode,e.type,t))}}function Qa(e){jn?Rn?Rn.push(e):Rn=[e]:jn=e}function Ka(){if(jn){var e=jn,t=Rn;if(Rn=jn=null,us(e),t)for(e=0;e>>=0,e===0?32:31-(Lp(e)/Op|0)|0}var Gr=64,Yr=4194304;function or(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Cl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=or(u):(o&=i,o!==0&&(r=or(o)))}else i=n&~l,i!==0?r=or(i):o!==0&&(r=or(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function $r(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-We(t),e[t]=n}function zp(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=cr),vs=String.fromCharCode(32),ys=!1;function dc(e,t){switch(e){case"keyup":return am.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function pc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var vn=!1;function fm(e,t){switch(e){case"compositionend":return pc(t);case"keypress":return t.which!==32?null:(ys=!0,vs);case"textInput":return e=t.data,e===vs&&ys?null:e;default:return null}}function dm(e,t){if(vn)return e==="compositionend"||!du&&dc(e,t)?(e=cc(),fl=au=Et=null,vn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=xs(n)}}function yc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?yc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function gc(){for(var e=window,t=Sl();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Sl(e.document)}return t}function pu(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function xm(e){var t=gc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&yc(n.ownerDocument.documentElement,n)){if(r!==null&&pu(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=ks(n,o);var i=ks(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,yn=null,mi=null,dr=null,hi=!1;function Es(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;hi||yn==null||yn!==Sl(r)||(r=yn,"selectionStart"in r&&pu(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),dr&&Cr(dr,r)||(dr=r,r=_l(mi,"onSelect"),0Sn||(e.current=xi[Sn],xi[Sn]=null,Sn--)}function B(e,t){Sn++,xi[Sn]=e.current,e.current=t}var Mt={},ce=$t(Mt),Se=$t(!1),Zt=Mt;function Mn(e,t){var n=e.type.contextTypes;if(!n)return Mt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function xe(e){return e=e.childContextTypes,e!=null}function Rl(){V(Se),V(ce)}function Ls(e,t,n){if(ce.current!==Mt)throw Error(x(168));B(ce,t),B(Se,n)}function _c(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(x(108,vp(e)||"Unknown",l));return X({},n,r)}function Ll(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Mt,Zt=ce.current,B(ce,e),B(Se,Se.current),!0}function Os(e,t,n){var r=e.stateNode;if(!r)throw Error(x(169));n?(e=_c(e,t,Zt),r.__reactInternalMemoizedMergedChildContext=e,V(Se),V(ce),B(ce,e)):V(Se),B(Se,n)}var nt=null,to=!1,Fo=!1;function jc(e){nt===null?nt=[e]:nt.push(e)}function Fm(e){to=!0,jc(e)}function It(){if(!Fo&&nt!==null){Fo=!0;var e=0,t=D;try{var n=nt;for(D=1;e>=i,l-=i,rt=1<<32-We(t)+l|n<j?(U=N,N=null):U=N.sibling;var P=d(p,N,v[j],S);if(P===null){N===null&&(N=U);break}e&&N&&P.alternate===null&&t(p,N),c=o(P,c,j),E===null?T=P:E.sibling=P,E=P,N=U}if(j===v.length)return n(p,N),Q&&At(p,j),T;if(N===null){for(;jj?(U=N,N=null):U=N.sibling;var ie=d(p,N,P.value,S);if(ie===null){N===null&&(N=U);break}e&&N&&ie.alternate===null&&t(p,N),c=o(ie,c,j),E===null?T=ie:E.sibling=ie,E=ie,N=U}if(P.done)return n(p,N),Q&&At(p,j),T;if(N===null){for(;!P.done;j++,P=v.next())P=h(p,P.value,S),P!==null&&(c=o(P,c,j),E===null?T=P:E.sibling=P,E=P);return Q&&At(p,j),T}for(N=r(p,N);!P.done;j++,P=v.next())P=w(N,p,j,P.value,S),P!==null&&(e&&P.alternate!==null&&N.delete(P.key===null?j:P.key),c=o(P,c,j),E===null?T=P:E.sibling=P,E=P);return e&&N.forEach(function(Ke){return t(p,Ke)}),Q&&At(p,j),T}function R(p,c,v,S){if(typeof v=="object"&&v!==null&&v.type===hn&&v.key===null&&(v=v.props.children),typeof v=="object"&&v!==null){switch(v.$$typeof){case Vr:e:{for(var T=v.key,E=c;E!==null;){if(E.key===T){if(T=v.type,T===hn){if(E.tag===7){n(p,E.sibling),c=l(E,v.props.children),c.return=p,p=c;break e}}else if(E.elementType===T||typeof T=="object"&&T!==null&&T.$$typeof===gt&&Ds(T)===E.type){n(p,E.sibling),c=l(E,v.props),c.ref=er(p,E,v),c.return=p,p=c;break e}n(p,E);break}else t(p,E);E=E.sibling}v.type===hn?(c=Yt(v.props.children,p.mode,S,v.key),c.return=p,p=c):(S=wl(v.type,v.key,v.props,null,p.mode,S),S.ref=er(p,c,v),S.return=p,p=S)}return i(p);case mn:e:{for(E=v.key;c!==null;){if(c.key===E)if(c.tag===4&&c.stateNode.containerInfo===v.containerInfo&&c.stateNode.implementation===v.implementation){n(p,c.sibling),c=l(c,v.children||[]),c.return=p,p=c;break e}else{n(p,c);break}else t(p,c);c=c.sibling}c=Bo(v,p.mode,S),c.return=p,p=c}return i(p);case gt:return E=v._init,R(p,c,E(v._payload),S)}if(lr(v))return g(p,c,v,S);if(Xn(v))return k(p,c,v,S);tl(p,v)}return typeof v=="string"&&v!==""||typeof v=="number"?(v=""+v,c!==null&&c.tag===6?(n(p,c.sibling),c=l(c,v),c.return=p,p=c):(n(p,c),c=Uo(v,p.mode,S),c.return=p,p=c),i(p)):n(p,c)}return R}var $n=$c(!0),Ic=$c(!1),Ar={},be=$t(Ar),jr=$t(Ar),Rr=$t(Ar);function Kt(e){if(e===Ar)throw Error(x(174));return e}function ku(e,t){switch(B(Rr,t),B(jr,e),B(be,Ar),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:li(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=li(t,e)}V(be),B(be,t)}function In(){V(be),V(jr),V(Rr)}function Dc(e){Kt(Rr.current);var t=Kt(be.current),n=li(t,e.type);t!==n&&(B(jr,e),B(be,n))}function Eu(e){jr.current===e&&(V(be),V(jr))}var G=$t(0);function $l(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if(t.flags&128)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var Mo=[];function Cu(){for(var e=0;en?n:4,e(!0);var r=zo.transition;zo.transition={};try{e(!1),t()}finally{D=n,zo.transition=r}}function ef(){return De().memoizedState}function Im(e,t,n){var r=Ot(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},tf(e))nf(t,n);else if(n=Pc(e,t,n,r),n!==null){var l=me();Ve(n,e,r,l),rf(n,t,r)}}function Dm(e,t,n){var r=Ot(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(tf(e))nf(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,Qe(u,i)){var s=t.interleaved;s===null?(l.next=l,Su(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=Pc(e,t,l,r),n!==null&&(l=me(),Ve(n,e,r,l),rf(n,t,r))}}function tf(e){var t=e.alternate;return e===Y||t!==null&&t===Y}function nf(e,t){pr=Il=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function rf(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,iu(e,n)}}var Dl={readContext:Ie,useCallback:ue,useContext:ue,useEffect:ue,useImperativeHandle:ue,useInsertionEffect:ue,useLayoutEffect:ue,useMemo:ue,useReducer:ue,useRef:ue,useState:ue,useDebugValue:ue,useDeferredValue:ue,useTransition:ue,useMutableSource:ue,useSyncExternalStore:ue,useId:ue,unstable_isNewReconciler:!1},Am={readContext:Ie,useCallback:function(e,t){return Ze().memoizedState=[e,t===void 0?null:t],e},useContext:Ie,useEffect:Us,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,hl(4194308,4,Xc.bind(null,t,e),n)},useLayoutEffect:function(e,t){return hl(4194308,4,e,t)},useInsertionEffect:function(e,t){return hl(4,2,e,t)},useMemo:function(e,t){var n=Ze();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ze();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Im.bind(null,Y,e),[r.memoizedState,e]},useRef:function(e){var t=Ze();return e={current:e},t.memoizedState=e},useState:As,useDebugValue:Ru,useDeferredValue:function(e){return Ze().memoizedState=e},useTransition:function(){var e=As(!1),t=e[0];return e=$m.bind(null,e[1]),Ze().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Y,l=Ze();if(Q){if(n===void 0)throw Error(x(407));n=n()}else{if(n=t(),ne===null)throw Error(x(349));qt&30||Bc(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Us(Wc.bind(null,r,o,e),[e]),r.flags|=2048,Pr(9,Hc.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ze(),t=ne.identifierPrefix;if(Q){var n=lt,r=rt;n=(r&~(1<<32-We(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Lr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Je]=t,e[_r]=r,mf(e,t,!1,!1),t.stateNode=e;e:{switch(i=ii(n,r),n){case"dialog":W("cancel",e),W("close",e),l=r;break;case"iframe":case"object":case"embed":W("load",e),l=r;break;case"video":case"audio":for(l=0;lAn&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304)}else{if(!r)if(e=Dl(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),tr(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!Q)return se(t),null}else 2*J()-o.renderingStartTime>An&&n!==1073741824&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=J(),t.sibling=null,n=G.current,B(G,r?n&1|2:n&1),t):(se(t),null);case 22:case 23:return zu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Ee&1073741824&&(se(t),t.subtreeFlags&6&&(t.flags|=8192)):se(t),null;case 24:return null;case 25:return null}throw Error(k(156,t.tag))}function Gm(e,t){switch(hu(t),t.tag){case 1:return xe(t.type)&&Ll(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Dn(),V(Se),V(ce),Cu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Eu(t),null;case 13:if(V(G),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(k(340));zn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return V(G),null;case 4:return Dn(),null;case 10:return wu(t.type._context),null;case 22:case 23:return zu(),null;case 24:return null;default:return null}}var rl=!1,ae=!1,Ym=typeof WeakSet=="function"?WeakSet:Set,_=null;function Cn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Z(e,t,r)}else n.current=null}function Fi(e,t,n){try{n()}catch(r){Z(e,t,r)}}var Xs=!1;function Xm(e,t){if(vi=Tl,e=wc(),pu(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,s=-1,a=0,m=0,h=e,f=null;t:for(;;){for(var w;h!==n||l!==0&&h.nodeType!==3||(u=i+l),h!==o||r!==0&&h.nodeType!==3||(s=i+r),h.nodeType===3&&(i+=h.nodeValue.length),(w=h.firstChild)!==null;)f=h,h=w;for(;;){if(h===e)break t;if(f===n&&++a===l&&(u=i),f===o&&++m===r&&(s=i),(w=h.nextSibling)!==null)break;h=f,f=h.parentNode}h=w}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(yi={focusedElem:e,selectionRange:n},Tl=!1,_=t;_!==null;)if(t=_,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,_=e;else for(;_!==null;){t=_;try{var g=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(g!==null){var x=g.memoizedProps,R=g.memoizedState,p=t.stateNode,c=p.getSnapshotBeforeUpdate(t.elementType===t.type?x:Ue(t.type,x),R);p.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var v=t.stateNode.containerInfo;v.nodeType===1?v.textContent="":v.nodeType===9&&v.documentElement&&v.removeChild(v.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(k(163))}}catch(S){Z(t,t.return,S)}if(e=t.sibling,e!==null){e.return=t.return,_=e;break}_=t.return}return g=Xs,Xs=!1,g}function mr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Fi(t,n,o)}l=l.next}while(l!==r)}}function lo(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Mi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function yf(e){var t=e.alternate;t!==null&&(e.alternate=null,yf(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Je],delete t[_r],delete t[Si],delete t[Om],delete t[Pm])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function gf(e){return e.tag===5||e.tag===3||e.tag===4}function Zs(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||gf(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function zi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Rl));else if(r!==4&&(e=e.child,e!==null))for(zi(e,t,n),e=e.sibling;e!==null;)zi(e,t,n),e=e.sibling}function $i(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for($i(e,t,n),e=e.sibling;e!==null;)$i(e,t,n),e=e.sibling}var re=null,Be=!1;function ht(e,t,n){for(n=n.child;n!==null;)wf(e,t,n),n=n.sibling}function wf(e,t,n){if(qe&&typeof qe.onCommitFiberUnmount=="function")try{qe.onCommitFiberUnmount(Zl,n)}catch{}switch(n.tag){case 5:ae||Cn(n,t);case 6:var r=re,l=Be;re=null,ht(e,t,n),re=r,Be=l,re!==null&&(Be?(e=re,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):re.removeChild(n.stateNode));break;case 18:re!==null&&(Be?(e=re,n=n.stateNode,e.nodeType===8?Po(e.parentNode,n):e.nodeType===1&&Po(e,n),kr(e)):Po(re,n.stateNode));break;case 4:r=re,l=Be,re=n.stateNode.containerInfo,Be=!0,ht(e,t,n),re=r,Be=l;break;case 0:case 11:case 14:case 15:if(!ae&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Fi(n,t,i),l=l.next}while(l!==r)}ht(e,t,n);break;case 1:if(!ae&&(Cn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){Z(n,t,u)}ht(e,t,n);break;case 21:ht(e,t,n);break;case 22:n.mode&1?(ae=(r=ae)||n.memoizedState!==null,ht(e,t,n),ae=r):ht(e,t,n);break;default:ht(e,t,n)}}function Js(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Ym),t.forEach(function(r){var l=lh.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Ae(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=J()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Jm(r/1960))-r,10e?16:e,Ct===null)var r=!1;else{if(e=Ct,Ct=null,Hl=0,z&6)throw Error(k(331));var l=z;for(z|=4,_=e.current;_!==null;){var o=_,i=o.child;if(_.flags&16){var u=o.deletions;if(u!==null){for(var s=0;sJ()-Fu?Gt(e,0):Pu|=n),ke(e,t)}function _f(e,t){t===0&&(e.mode&1?(t=Yr,Yr<<=1,!(Yr&130023424)&&(Yr=4194304)):t=1);var n=me();e=at(e,t),e!==null&&($r(e,t,n),ke(e,n))}function rh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),_f(e,n)}function lh(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(k(314))}r!==null&&r.delete(t),_f(e,n)}var jf;jf=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Se.current)we=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return we=!1,Qm(e,t,n);we=!!(e.flags&131072)}else we=!1,Q&&t.flags&1048576&&Lc(t,Fl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;vl(e,t),e=t.pendingProps;var l=Mn(t,ce.current);On(t,n),l=Tu(null,t,r,e,l,n);var o=_u();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,xe(r)?(o=!0,Ol(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,xu(t),l.updater=no,t.stateNode=l,l._reactInternals=t,Ti(t,r,e,n),t=Ri(null,t,r,!0,o,n)):(t.tag=0,Q&&o&&mu(t),de(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(vl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=ih(r),e=Ue(r,e),l){case 0:t=ji(null,t,r,e,n);break e;case 1:t=Ks(null,t,r,e,n);break e;case 11:t=Vs(null,t,r,e,n);break e;case 14:t=Qs(null,t,r,Ue(r.type,e),n);break e}throw Error(k(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ue(r,l),ji(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ue(r,l),Ks(e,t,r,l,n);case 3:e:{if(ff(t),e===null)throw Error(k(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Mc(e,t),$l(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=In(Error(k(423)),t),t=Gs(e,t,r,n,l);break e}else if(r!==l){l=In(Error(k(424)),t),t=Gs(e,t,r,n,l);break e}else for(Ce=jt(t.stateNode.containerInfo.firstChild),Ne=t,Q=!0,He=null,n=Ic(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(zn(),r===l){t=ct(e,t,n);break e}de(e,t,r,n)}t=t.child}return t;case 5:return Ac(t),e===null&&Ei(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,gi(r,l)?i=null:o!==null&&gi(r,o)&&(t.flags|=32),cf(e,t),de(e,t,i,n),t.child;case 6:return e===null&&Ei(t),null;case 13:return df(e,t,n);case 4:return ku(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=$n(t,null,r,n):de(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ue(r,l),Vs(e,t,r,l,n);case 7:return de(e,t,t.pendingProps,n),t.child;case 8:return de(e,t,t.pendingProps.children,n),t.child;case 12:return de(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,B(Ml,r._currentValue),r._currentValue=i,o!==null)if(Qe(o.value,i)){if(o.children===l.children&&!Se.current){t=ct(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=ot(-1,n&-n),s.tag=2;var a=o.updateQueue;if(a!==null){a=a.shared;var m=a.pending;m===null?s.next=s:(s.next=m.next,m.next=s),a.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),Ci(o.return,n,t),u.lanes|=n;break}s=s.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(k(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),Ci(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}de(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,On(t,n),l=De(l),r=r(l),t.flags|=1,de(e,t,r,n),t.child;case 14:return r=t.type,l=Ue(r,t.pendingProps),l=Ue(r.type,l),Qs(e,t,r,l,n);case 15:return sf(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ue(r,l),vl(e,t),t.tag=1,xe(r)?(e=!0,Ol(t)):e=!1,On(t,n),$c(t,r,l),Ti(t,r,l,n),Ri(null,t,r,!0,e,n);case 19:return pf(e,t,n);case 22:return af(e,t,n)}throw Error(k(156,t.tag))};function Rf(e,t){return ec(e,t)}function oh(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function ze(e,t,n,r){return new oh(e,t,n,r)}function Du(e){return e=e.prototype,!(!e||!e.isReactComponent)}function ih(e){if(typeof e=="function")return Du(e)?1:0;if(e!=null){if(e=e.$$typeof,e===nu)return 11;if(e===ru)return 14}return 2}function Pt(e,t){var n=e.alternate;return n===null?(n=ze(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function wl(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Du(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case hn:return Yt(n.children,l,o,t);case tu:i=8,l|=8;break;case Zo:return e=ze(12,n,t,l|2),e.elementType=Zo,e.lanes=o,e;case Jo:return e=ze(13,n,t,l),e.elementType=Jo,e.lanes=o,e;case qo:return e=ze(19,n,t,l),e.elementType=qo,e.lanes=o,e;case Da:return io(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case za:i=10;break e;case $a:i=9;break e;case nu:i=11;break e;case ru:i=14;break e;case gt:i=16,r=null;break e}throw Error(k(130,e==null?e:typeof e,""))}return t=ze(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Yt(e,t,n,r){return e=ze(7,e,r,t),e.lanes=n,e}function io(e,t,n,r){return e=ze(22,e,r,t),e.elementType=Da,e.lanes=n,e.stateNode={isHidden:!1},e}function Uo(e,t,n){return e=ze(6,e,null,t),e.lanes=n,e}function Bo(e,t,n){return t=ze(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function uh(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=xo(0),this.expirationTimes=xo(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=xo(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Iu(e,t,n,r,l,o,i,u,s){return e=new uh(e,t,n,u,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=ze(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},xu(o),e}function sh(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Ff)}catch(e){console.error(e)}}Ff(),La.exports=_e;var Mf=La.exports;const Tn=Yl(Mf);var oa=Mf;Yo.createRoot=oa.createRoot,Yo.hydrateRoot=oa.hydrateRoot;var zf={exports:{}};/*! +`+o.stack}return{value:e,source:t,stack:l,digest:null}}function Do(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function _i(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}var Hm=typeof WeakMap=="function"?WeakMap:Map;function lf(e,t,n){n=ot(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){Ul||(Ul=!0,Ii=r),_i(e,t)},n}function of(e,t,n){n=ot(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r=="function"){var l=t.value;n.payload=function(){return r(l)},n.callback=function(){_i(e,t)}}var o=e.stateNode;return o!==null&&typeof o.componentDidCatch=="function"&&(n.callback=function(){_i(e,t),typeof r!="function"&&(Lt===null?Lt=new Set([this]):Lt.add(this));var i=t.stack;this.componentDidCatch(t.value,{componentStack:i!==null?i:""})}),n}function Bs(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Hm;var l=new Set;r.set(t,l)}else l=r.get(t),l===void 0&&(l=new Set,r.set(t,l));l.has(n)||(l.add(n),e=nh.bind(null,e,t,n),t.then(e,e))}function Hs(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function Ws(e,t,n,r,l){return e.mode&1?(e.flags|=65536,e.lanes=l,e):(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=ot(-1,1),t.tag=2,Rt(n,t,1))),n.lanes|=1),e)}var Wm=dt.ReactCurrentOwner,we=!1;function de(e,t,n,r){t.child=e===null?Ic(t,null,n,r):$n(t,e.child,n,r)}function Vs(e,t,n,r,l){n=n.render;var o=t.ref;return On(t,l),r=Tu(e,t,n,r,o,l),n=_u(),e!==null&&!we?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,ct(e,t,l)):(Q&&n&&mu(t),t.flags|=1,de(e,t,r,l),t.child)}function Qs(e,t,n,r,l){if(e===null){var o=n.type;return typeof o=="function"&&!Iu(o)&&o.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=o,uf(e,t,o,r,l)):(e=wl(n.type,null,r,t,t.mode,l),e.ref=t.ref,e.return=t,t.child=e)}if(o=e.child,!(e.lanes&l)){var i=o.memoizedProps;if(n=n.compare,n=n!==null?n:Cr,n(i,r)&&e.ref===t.ref)return ct(e,t,l)}return t.flags|=1,e=Pt(o,r),e.ref=t.ref,e.return=t,t.child=e}function uf(e,t,n,r,l){if(e!==null){var o=e.memoizedProps;if(Cr(o,r)&&e.ref===t.ref)if(we=!1,t.pendingProps=r=o,(e.lanes&l)!==0)e.flags&131072&&(we=!0);else return t.lanes=e.lanes,ct(e,t,l)}return ji(e,t,n,r,l)}function sf(e,t,n){var r=t.pendingProps,l=r.children,o=e!==null?e.memoizedState:null;if(r.mode==="hidden")if(!(t.mode&1))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},B(Nn,Ee),Ee|=n;else{if(!(n&1073741824))return e=o!==null?o.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,B(Nn,Ee),Ee|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=o!==null?o.baseLanes:n,B(Nn,Ee),Ee|=r}else o!==null?(r=o.baseLanes|n,t.memoizedState=null):r=n,B(Nn,Ee),Ee|=r;return de(e,t,l,n),t.child}function af(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function ji(e,t,n,r,l){var o=xe(n)?Zt:ce.current;return o=Mn(t,o),On(t,l),n=Tu(e,t,n,r,o,l),r=_u(),e!==null&&!we?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,ct(e,t,l)):(Q&&r&&mu(t),t.flags|=1,de(e,t,n,l),t.child)}function Ks(e,t,n,r,l){if(xe(n)){var o=!0;Ll(t)}else o=!1;if(On(t,l),t.stateNode===null)vl(e,t),zc(t,n,r),Ti(t,n,r,l),r=!0;else if(e===null){var i=t.stateNode,u=t.memoizedProps;i.props=u;var s=i.context,a=n.contextType;typeof a=="object"&&a!==null?a=Ie(a):(a=xe(n)?Zt:ce.current,a=Mn(t,a));var m=n.getDerivedStateFromProps,h=typeof m=="function"||typeof i.getSnapshotBeforeUpdate=="function";h||typeof i.UNSAFE_componentWillReceiveProps!="function"&&typeof i.componentWillReceiveProps!="function"||(u!==r||s!==a)&&Is(t,i,r,a),wt=!1;var d=t.memoizedState;i.state=d,zl(t,r,i,l),s=t.memoizedState,u!==r||d!==s||Se.current||wt?(typeof m=="function"&&(Ni(t,n,m,r),s=t.memoizedState),(u=wt||$s(t,n,u,r,d,s,a))?(h||typeof i.UNSAFE_componentWillMount!="function"&&typeof i.componentWillMount!="function"||(typeof i.componentWillMount=="function"&&i.componentWillMount(),typeof i.UNSAFE_componentWillMount=="function"&&i.UNSAFE_componentWillMount()),typeof i.componentDidMount=="function"&&(t.flags|=4194308)):(typeof i.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=s),i.props=r,i.state=s,i.context=a,r=u):(typeof i.componentDidMount=="function"&&(t.flags|=4194308),r=!1)}else{i=t.stateNode,Fc(e,t),u=t.memoizedProps,a=t.type===t.elementType?u:Ue(t.type,u),i.props=a,h=t.pendingProps,d=i.context,s=n.contextType,typeof s=="object"&&s!==null?s=Ie(s):(s=xe(n)?Zt:ce.current,s=Mn(t,s));var w=n.getDerivedStateFromProps;(m=typeof w=="function"||typeof i.getSnapshotBeforeUpdate=="function")||typeof i.UNSAFE_componentWillReceiveProps!="function"&&typeof i.componentWillReceiveProps!="function"||(u!==h||d!==s)&&Is(t,i,r,s),wt=!1,d=t.memoizedState,i.state=d,zl(t,r,i,l);var g=t.memoizedState;u!==h||d!==g||Se.current||wt?(typeof w=="function"&&(Ni(t,n,w,r),g=t.memoizedState),(a=wt||$s(t,n,a,r,d,g,s)||!1)?(m||typeof i.UNSAFE_componentWillUpdate!="function"&&typeof i.componentWillUpdate!="function"||(typeof i.componentWillUpdate=="function"&&i.componentWillUpdate(r,g,s),typeof i.UNSAFE_componentWillUpdate=="function"&&i.UNSAFE_componentWillUpdate(r,g,s)),typeof i.componentDidUpdate=="function"&&(t.flags|=4),typeof i.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof i.componentDidUpdate!="function"||u===e.memoizedProps&&d===e.memoizedState||(t.flags|=4),typeof i.getSnapshotBeforeUpdate!="function"||u===e.memoizedProps&&d===e.memoizedState||(t.flags|=1024),t.memoizedProps=r,t.memoizedState=g),i.props=r,i.state=g,i.context=s,r=a):(typeof i.componentDidUpdate!="function"||u===e.memoizedProps&&d===e.memoizedState||(t.flags|=4),typeof i.getSnapshotBeforeUpdate!="function"||u===e.memoizedProps&&d===e.memoizedState||(t.flags|=1024),r=!1)}return Ri(e,t,n,r,o,l)}function Ri(e,t,n,r,l,o){af(e,t);var i=(t.flags&128)!==0;if(!r&&!i)return l&&Os(t,n,!1),ct(e,t,o);r=t.stateNode,Wm.current=t;var u=i&&typeof n.getDerivedStateFromError!="function"?null:r.render();return t.flags|=1,e!==null&&i?(t.child=$n(t,e.child,null,o),t.child=$n(t,null,u,o)):de(e,t,u,o),t.memoizedState=r.state,l&&Os(t,n,!0),t.child}function cf(e){var t=e.stateNode;t.pendingContext?Ls(e,t.pendingContext,t.pendingContext!==t.context):t.context&&Ls(e,t.context,!1),ku(e,t.containerInfo)}function Gs(e,t,n,r,l){return zn(),vu(l),t.flags|=256,de(e,t,n,r),t.child}var Li={dehydrated:null,treeContext:null,retryLane:0};function Oi(e){return{baseLanes:e,cachePool:null,transitions:null}}function ff(e,t,n){var r=t.pendingProps,l=G.current,o=!1,i=(t.flags&128)!==0,u;if((u=i)||(u=e!==null&&e.memoizedState===null?!1:(l&2)!==0),u?(o=!0,t.flags&=-129):(e===null||e.memoizedState!==null)&&(l|=1),B(G,l&1),e===null)return Ei(t),e=t.memoizedState,e!==null&&(e=e.dehydrated,e!==null)?(t.mode&1?e.data==="$!"?t.lanes=8:t.lanes=1073741824:t.lanes=1,null):(i=r.children,e=r.fallback,o?(r=t.mode,o=t.child,i={mode:"hidden",children:i},!(r&1)&&o!==null?(o.childLanes=0,o.pendingProps=i):o=io(i,r,0,null),e=Yt(e,r,n,null),o.return=t,e.return=t,o.sibling=e,t.child=o,t.child.memoizedState=Oi(n),t.memoizedState=Li,e):Lu(t,i));if(l=e.memoizedState,l!==null&&(u=l.dehydrated,u!==null))return Vm(e,t,i,r,u,l,n);if(o){o=r.fallback,i=t.mode,l=e.child,u=l.sibling;var s={mode:"hidden",children:r.children};return!(i&1)&&t.child!==l?(r=t.child,r.childLanes=0,r.pendingProps=s,t.deletions=null):(r=Pt(l,s),r.subtreeFlags=l.subtreeFlags&14680064),u!==null?o=Pt(u,o):(o=Yt(o,i,n,null),o.flags|=2),o.return=t,r.return=t,r.sibling=o,t.child=r,r=o,o=t.child,i=e.child.memoizedState,i=i===null?Oi(n):{baseLanes:i.baseLanes|n,cachePool:null,transitions:i.transitions},o.memoizedState=i,o.childLanes=e.childLanes&~n,t.memoizedState=Li,r}return o=e.child,e=o.sibling,r=Pt(o,{mode:"visible",children:r.children}),!(t.mode&1)&&(r.lanes=n),r.return=t,r.sibling=null,e!==null&&(n=t.deletions,n===null?(t.deletions=[e],t.flags|=16):n.push(e)),t.child=r,t.memoizedState=null,r}function Lu(e,t){return t=io({mode:"visible",children:t},e.mode,0,null),t.return=e,e.child=t}function nl(e,t,n,r){return r!==null&&vu(r),$n(t,e.child,null,n),e=Lu(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function Vm(e,t,n,r,l,o,i){if(n)return t.flags&256?(t.flags&=-257,r=Do(Error(x(422))),nl(e,t,i,r)):t.memoizedState!==null?(t.child=e.child,t.flags|=128,null):(o=r.fallback,l=t.mode,r=io({mode:"visible",children:r.children},l,0,null),o=Yt(o,l,i,null),o.flags|=2,r.return=t,o.return=t,r.sibling=o,t.child=r,t.mode&1&&$n(t,e.child,null,i),t.child.memoizedState=Oi(i),t.memoizedState=Li,o);if(!(t.mode&1))return nl(e,t,i,null);if(l.data==="$!"){if(r=l.nextSibling&&l.nextSibling.dataset,r)var u=r.dgst;return r=u,o=Error(x(419)),r=Do(o,r,void 0),nl(e,t,i,r)}if(u=(i&e.childLanes)!==0,we||u){if(r=ne,r!==null){switch(i&-i){case 4:l=2;break;case 16:l=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:l=32;break;case 536870912:l=268435456;break;default:l=0}l=l&(r.suspendedLanes|i)?0:l,l!==0&&l!==o.retryLane&&(o.retryLane=l,at(e,l),Ve(r,e,l,-1))}return $u(),r=Do(Error(x(421))),nl(e,t,i,r)}return l.data==="$?"?(t.flags|=128,t.child=e.child,t=rh.bind(null,e),l._reactRetry=t,null):(e=o.treeContext,Ce=jt(l.nextSibling),Ne=t,Q=!0,He=null,e!==null&&(Pe[Fe++]=rt,Pe[Fe++]=lt,Pe[Fe++]=Jt,rt=e.id,lt=e.overflow,Jt=t),t=Lu(t,r.children),t.flags|=4096,t)}function Ys(e,t,n){e.lanes|=t;var r=e.alternate;r!==null&&(r.lanes|=t),Ci(e.return,t,n)}function Ao(e,t,n,r,l){var o=e.memoizedState;o===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:r,tail:n,tailMode:l}:(o.isBackwards=t,o.rendering=null,o.renderingStartTime=0,o.last=r,o.tail=n,o.tailMode=l)}function df(e,t,n){var r=t.pendingProps,l=r.revealOrder,o=r.tail;if(de(e,t,r.children,n),r=G.current,r&2)r=r&1|2,t.flags|=128;else{if(e!==null&&e.flags&128)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&Ys(e,n,t);else if(e.tag===19)Ys(e,n,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}r&=1}if(B(G,r),!(t.mode&1))t.memoizedState=null;else switch(l){case"forwards":for(n=t.child,l=null;n!==null;)e=n.alternate,e!==null&&$l(e)===null&&(l=n),n=n.sibling;n=l,n===null?(l=t.child,t.child=null):(l=n.sibling,n.sibling=null),Ao(t,!1,l,n,o);break;case"backwards":for(n=null,l=t.child,t.child=null;l!==null;){if(e=l.alternate,e!==null&&$l(e)===null){t.child=l;break}e=l.sibling,l.sibling=n,n=l,l=e}Ao(t,!0,n,null,o);break;case"together":Ao(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function vl(e,t){!(t.mode&1)&&e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2)}function ct(e,t,n){if(e!==null&&(t.dependencies=e.dependencies),bt|=t.lanes,!(n&t.childLanes))return null;if(e!==null&&t.child!==e.child)throw Error(x(153));if(t.child!==null){for(e=t.child,n=Pt(e,e.pendingProps),t.child=n,n.return=t;e.sibling!==null;)e=e.sibling,n=n.sibling=Pt(e,e.pendingProps),n.return=t;n.sibling=null}return t.child}function Qm(e,t,n){switch(t.tag){case 3:cf(t),zn();break;case 5:Dc(t);break;case 1:xe(t.type)&&Ll(t);break;case 4:ku(t,t.stateNode.containerInfo);break;case 10:var r=t.type._context,l=t.memoizedProps.value;B(Fl,r._currentValue),r._currentValue=l;break;case 13:if(r=t.memoizedState,r!==null)return r.dehydrated!==null?(B(G,G.current&1),t.flags|=128,null):n&t.child.childLanes?ff(e,t,n):(B(G,G.current&1),e=ct(e,t,n),e!==null?e.sibling:null);B(G,G.current&1);break;case 19:if(r=(n&t.childLanes)!==0,e.flags&128){if(r)return df(e,t,n);t.flags|=128}if(l=t.memoizedState,l!==null&&(l.rendering=null,l.tail=null,l.lastEffect=null),B(G,G.current),r)break;return null;case 22:case 23:return t.lanes=0,sf(e,t,n)}return ct(e,t,n)}var pf,Pi,mf,hf;pf=function(e,t){for(var n=t.child;n!==null;){if(n.tag===5||n.tag===6)e.appendChild(n.stateNode);else if(n.tag!==4&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return;n=n.return}n.sibling.return=n.return,n=n.sibling}};Pi=function(){};mf=function(e,t,n,r){var l=e.memoizedProps;if(l!==r){e=t.stateNode,Kt(be.current);var o=null;switch(n){case"input":l=ei(e,l),r=ei(e,r),o=[];break;case"select":l=X({},l,{value:void 0}),r=X({},r,{value:void 0}),o=[];break;case"textarea":l=ri(e,l),r=ri(e,r),o=[];break;default:typeof l.onClick!="function"&&typeof r.onClick=="function"&&(e.onclick=jl)}oi(n,r);var i;n=null;for(a in l)if(!r.hasOwnProperty(a)&&l.hasOwnProperty(a)&&l[a]!=null)if(a==="style"){var u=l[a];for(i in u)u.hasOwnProperty(i)&&(n||(n={}),n[i]="")}else a!=="dangerouslySetInnerHTML"&&a!=="children"&&a!=="suppressContentEditableWarning"&&a!=="suppressHydrationWarning"&&a!=="autoFocus"&&(yr.hasOwnProperty(a)?o||(o=[]):(o=o||[]).push(a,null));for(a in r){var s=r[a];if(u=l!=null?l[a]:void 0,r.hasOwnProperty(a)&&s!==u&&(s!=null||u!=null))if(a==="style")if(u){for(i in u)!u.hasOwnProperty(i)||s&&s.hasOwnProperty(i)||(n||(n={}),n[i]="");for(i in s)s.hasOwnProperty(i)&&u[i]!==s[i]&&(n||(n={}),n[i]=s[i])}else n||(o||(o=[]),o.push(a,n)),n=s;else a==="dangerouslySetInnerHTML"?(s=s?s.__html:void 0,u=u?u.__html:void 0,s!=null&&u!==s&&(o=o||[]).push(a,s)):a==="children"?typeof s!="string"&&typeof s!="number"||(o=o||[]).push(a,""+s):a!=="suppressContentEditableWarning"&&a!=="suppressHydrationWarning"&&(yr.hasOwnProperty(a)?(s!=null&&a==="onScroll"&&W("scroll",e),o||u===s||(o=[])):(o=o||[]).push(a,s))}n&&(o=o||[]).push("style",n);var a=o;(t.updateQueue=a)&&(t.flags|=4)}};hf=function(e,t,n,r){n!==r&&(t.flags|=4)};function tr(e,t){if(!Q)switch(e.tailMode){case"hidden":t=e.tail;for(var n=null;t!==null;)t.alternate!==null&&(n=t),t=t.sibling;n===null?e.tail=null:n.sibling=null;break;case"collapsed":n=e.tail;for(var r=null;n!==null;)n.alternate!==null&&(r=n),n=n.sibling;r===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:r.sibling=null}}function se(e){var t=e.alternate!==null&&e.alternate.child===e.child,n=0,r=0;if(t)for(var l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags&14680064,r|=l.flags&14680064,l.return=e,l=l.sibling;else for(l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags,r|=l.flags,l.return=e,l=l.sibling;return e.subtreeFlags|=r,e.childLanes=n,t}function Km(e,t,n){var r=t.pendingProps;switch(hu(t),t.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return se(t),null;case 1:return xe(t.type)&&Rl(),se(t),null;case 3:return r=t.stateNode,In(),V(Se),V(ce),Cu(),r.pendingContext&&(r.context=r.pendingContext,r.pendingContext=null),(e===null||e.child===null)&&(el(t)?t.flags|=4:e===null||e.memoizedState.isDehydrated&&!(t.flags&256)||(t.flags|=1024,He!==null&&(Ui(He),He=null))),Pi(e,t),se(t),null;case 5:Eu(t);var l=Kt(Rr.current);if(n=t.type,e!==null&&t.stateNode!=null)mf(e,t,n,r,l),e.ref!==t.ref&&(t.flags|=512,t.flags|=2097152);else{if(!r){if(t.stateNode===null)throw Error(x(166));return se(t),null}if(e=Kt(be.current),el(t)){r=t.stateNode,n=t.type;var o=t.memoizedProps;switch(r[Je]=t,r[_r]=o,e=(t.mode&1)!==0,n){case"dialog":W("cancel",r),W("close",r);break;case"iframe":case"object":case"embed":W("load",r);break;case"video":case"audio":for(l=0;l<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Je]=t,e[_r]=r,pf(e,t,!1,!1),t.stateNode=e;e:{switch(i=ii(n,r),n){case"dialog":W("cancel",e),W("close",e),l=r;break;case"iframe":case"object":case"embed":W("load",e),l=r;break;case"video":case"audio":for(l=0;lAn&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304)}else{if(!r)if(e=$l(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),tr(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!Q)return se(t),null}else 2*J()-o.renderingStartTime>An&&n!==1073741824&&(t.flags|=128,r=!0,tr(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=J(),t.sibling=null,n=G.current,B(G,r?n&1|2:n&1),t):(se(t),null);case 22:case 23:return zu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Ee&1073741824&&(se(t),t.subtreeFlags&6&&(t.flags|=8192)):se(t),null;case 24:return null;case 25:return null}throw Error(x(156,t.tag))}function Gm(e,t){switch(hu(t),t.tag){case 1:return xe(t.type)&&Rl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return In(),V(Se),V(ce),Cu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Eu(t),null;case 13:if(V(G),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(x(340));zn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return V(G),null;case 4:return In(),null;case 10:return wu(t.type._context),null;case 22:case 23:return zu(),null;case 24:return null;default:return null}}var rl=!1,ae=!1,Ym=typeof WeakSet=="function"?WeakSet:Set,_=null;function Cn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Z(e,t,r)}else n.current=null}function Fi(e,t,n){try{n()}catch(r){Z(e,t,r)}}var Xs=!1;function Xm(e,t){if(vi=Nl,e=gc(),pu(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,s=-1,a=0,m=0,h=e,d=null;t:for(;;){for(var w;h!==n||l!==0&&h.nodeType!==3||(u=i+l),h!==o||r!==0&&h.nodeType!==3||(s=i+r),h.nodeType===3&&(i+=h.nodeValue.length),(w=h.firstChild)!==null;)d=h,h=w;for(;;){if(h===e)break t;if(d===n&&++a===l&&(u=i),d===o&&++m===r&&(s=i),(w=h.nextSibling)!==null)break;h=d,d=h.parentNode}h=w}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(yi={focusedElem:e,selectionRange:n},Nl=!1,_=t;_!==null;)if(t=_,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,_=e;else for(;_!==null;){t=_;try{var g=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(g!==null){var k=g.memoizedProps,R=g.memoizedState,p=t.stateNode,c=p.getSnapshotBeforeUpdate(t.elementType===t.type?k:Ue(t.type,k),R);p.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var v=t.stateNode.containerInfo;v.nodeType===1?v.textContent="":v.nodeType===9&&v.documentElement&&v.removeChild(v.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(x(163))}}catch(S){Z(t,t.return,S)}if(e=t.sibling,e!==null){e.return=t.return,_=e;break}_=t.return}return g=Xs,Xs=!1,g}function mr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Fi(t,n,o)}l=l.next}while(l!==r)}}function lo(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Mi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function vf(e){var t=e.alternate;t!==null&&(e.alternate=null,vf(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Je],delete t[_r],delete t[Si],delete t[Om],delete t[Pm])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function yf(e){return e.tag===5||e.tag===3||e.tag===4}function Zs(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||yf(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function zi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=jl));else if(r!==4&&(e=e.child,e!==null))for(zi(e,t,n),e=e.sibling;e!==null;)zi(e,t,n),e=e.sibling}function $i(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for($i(e,t,n),e=e.sibling;e!==null;)$i(e,t,n),e=e.sibling}var re=null,Be=!1;function ht(e,t,n){for(n=n.child;n!==null;)gf(e,t,n),n=n.sibling}function gf(e,t,n){if(qe&&typeof qe.onCommitFiberUnmount=="function")try{qe.onCommitFiberUnmount(Zl,n)}catch{}switch(n.tag){case 5:ae||Cn(n,t);case 6:var r=re,l=Be;re=null,ht(e,t,n),re=r,Be=l,re!==null&&(Be?(e=re,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):re.removeChild(n.stateNode));break;case 18:re!==null&&(Be?(e=re,n=n.stateNode,e.nodeType===8?Po(e.parentNode,n):e.nodeType===1&&Po(e,n),kr(e)):Po(re,n.stateNode));break;case 4:r=re,l=Be,re=n.stateNode.containerInfo,Be=!0,ht(e,t,n),re=r,Be=l;break;case 0:case 11:case 14:case 15:if(!ae&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Fi(n,t,i),l=l.next}while(l!==r)}ht(e,t,n);break;case 1:if(!ae&&(Cn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){Z(n,t,u)}ht(e,t,n);break;case 21:ht(e,t,n);break;case 22:n.mode&1?(ae=(r=ae)||n.memoizedState!==null,ht(e,t,n),ae=r):ht(e,t,n);break;default:ht(e,t,n)}}function Js(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Ym),t.forEach(function(r){var l=lh.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Ae(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=J()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Jm(r/1960))-r,10e?16:e,Ct===null)var r=!1;else{if(e=Ct,Ct=null,Bl=0,z&6)throw Error(x(331));var l=z;for(z|=4,_=e.current;_!==null;){var o=_,i=o.child;if(_.flags&16){var u=o.deletions;if(u!==null){for(var s=0;sJ()-Fu?Gt(e,0):Pu|=n),ke(e,t)}function Tf(e,t){t===0&&(e.mode&1?(t=Yr,Yr<<=1,!(Yr&130023424)&&(Yr=4194304)):t=1);var n=me();e=at(e,t),e!==null&&($r(e,t,n),ke(e,n))}function rh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Tf(e,n)}function lh(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(x(314))}r!==null&&r.delete(t),Tf(e,n)}var _f;_f=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Se.current)we=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return we=!1,Qm(e,t,n);we=!!(e.flags&131072)}else we=!1,Q&&t.flags&1048576&&Rc(t,Pl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;vl(e,t),e=t.pendingProps;var l=Mn(t,ce.current);On(t,n),l=Tu(null,t,r,e,l,n);var o=_u();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,xe(r)?(o=!0,Ll(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,xu(t),l.updater=no,t.stateNode=l,l._reactInternals=t,Ti(t,r,e,n),t=Ri(null,t,r,!0,o,n)):(t.tag=0,Q&&o&&mu(t),de(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(vl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=ih(r),e=Ue(r,e),l){case 0:t=ji(null,t,r,e,n);break e;case 1:t=Ks(null,t,r,e,n);break e;case 11:t=Vs(null,t,r,e,n);break e;case 14:t=Qs(null,t,r,Ue(r.type,e),n);break e}throw Error(x(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ue(r,l),ji(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ue(r,l),Ks(e,t,r,l,n);case 3:e:{if(cf(t),e===null)throw Error(x(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Fc(e,t),zl(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=Dn(Error(x(423)),t),t=Gs(e,t,r,n,l);break e}else if(r!==l){l=Dn(Error(x(424)),t),t=Gs(e,t,r,n,l);break e}else for(Ce=jt(t.stateNode.containerInfo.firstChild),Ne=t,Q=!0,He=null,n=Ic(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(zn(),r===l){t=ct(e,t,n);break e}de(e,t,r,n)}t=t.child}return t;case 5:return Dc(t),e===null&&Ei(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,gi(r,l)?i=null:o!==null&&gi(r,o)&&(t.flags|=32),af(e,t),de(e,t,i,n),t.child;case 6:return e===null&&Ei(t),null;case 13:return ff(e,t,n);case 4:return ku(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=$n(t,null,r,n):de(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ue(r,l),Vs(e,t,r,l,n);case 7:return de(e,t,t.pendingProps,n),t.child;case 8:return de(e,t,t.pendingProps.children,n),t.child;case 12:return de(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,B(Fl,r._currentValue),r._currentValue=i,o!==null)if(Qe(o.value,i)){if(o.children===l.children&&!Se.current){t=ct(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=ot(-1,n&-n),s.tag=2;var a=o.updateQueue;if(a!==null){a=a.shared;var m=a.pending;m===null?s.next=s:(s.next=m.next,m.next=s),a.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),Ci(o.return,n,t),u.lanes|=n;break}s=s.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(x(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),Ci(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}de(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,On(t,n),l=Ie(l),r=r(l),t.flags|=1,de(e,t,r,n),t.child;case 14:return r=t.type,l=Ue(r,t.pendingProps),l=Ue(r.type,l),Qs(e,t,r,l,n);case 15:return uf(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ue(r,l),vl(e,t),t.tag=1,xe(r)?(e=!0,Ll(t)):e=!1,On(t,n),zc(t,r,l),Ti(t,r,l,n),Ri(null,t,r,!0,e,n);case 19:return df(e,t,n);case 22:return sf(e,t,n)}throw Error(x(156,t.tag))};function jf(e,t){return ba(e,t)}function oh(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function ze(e,t,n,r){return new oh(e,t,n,r)}function Iu(e){return e=e.prototype,!(!e||!e.isReactComponent)}function ih(e){if(typeof e=="function")return Iu(e)?1:0;if(e!=null){if(e=e.$$typeof,e===nu)return 11;if(e===ru)return 14}return 2}function Pt(e,t){var n=e.alternate;return n===null?(n=ze(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function wl(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Iu(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case hn:return Yt(n.children,l,o,t);case tu:i=8,l|=8;break;case Zo:return e=ze(12,n,t,l|2),e.elementType=Zo,e.lanes=o,e;case Jo:return e=ze(13,n,t,l),e.elementType=Jo,e.lanes=o,e;case qo:return e=ze(19,n,t,l),e.elementType=qo,e.lanes=o,e;case $a:return io(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ma:i=10;break e;case za:i=9;break e;case nu:i=11;break e;case ru:i=14;break e;case gt:i=16,r=null;break e}throw Error(x(130,e==null?e:typeof e,""))}return t=ze(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Yt(e,t,n,r){return e=ze(7,e,r,t),e.lanes=n,e}function io(e,t,n,r){return e=ze(22,e,r,t),e.elementType=$a,e.lanes=n,e.stateNode={isHidden:!1},e}function Uo(e,t,n){return e=ze(6,e,null,t),e.lanes=n,e}function Bo(e,t,n){return t=ze(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function uh(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=xo(0),this.expirationTimes=xo(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=xo(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Du(e,t,n,r,l,o,i,u,s){return e=new uh(e,t,n,u,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=ze(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},xu(o),e}function sh(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Pf)}catch(e){console.error(e)}}Pf(),Ra.exports=_e;var Ff=Ra.exports;const Tn=Yl(Ff);var oa=Ff;Yo.createRoot=oa.createRoot,Yo.hydrateRoot=oa.hydrateRoot;var Mf={exports:{}};/*! Copyright (c) 2018 Jed Watson. Licensed under the MIT License (MIT), see http://jedwatson.github.io/classnames -*/(function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],l=0;l=0)&&(n[l]=e[l]);return n}function ia(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function mh(e){var t=hh(e,"string");return typeof t=="symbol"?t:String(t)}function hh(e,t){if(typeof e!="object"||e===null)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||"default");if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function vh(e,t,n){var r=y.useRef(e!==void 0),l=y.useState(t),o=l[0],i=l[1],u=e!==void 0,s=r.current;return r.current=u,!u&&s&&o!==t&&i(t),[u?e:o,y.useCallback(function(a){for(var m=arguments.length,h=new Array(m>1?m-1:0),f=1;f{o.target===e&&(l(),t(o))},n+r)}function Uh(e){e.offsetHeight}const aa=e=>!e||typeof e=="function"?e:t=>{e.current=t};function Bh(e,t){const n=aa(e),r=aa(t);return l=>{n&&n(l),r&&r(l)}}function mo(e,t){return y.useMemo(()=>Bh(e,t),[e,t])}function Hh(e){return e&&"setState"in e?Tn.findDOMNode(e):e??null}const Wh=Wt.forwardRef(({onEnter:e,onEntering:t,onEntered:n,onExit:r,onExiting:l,onExited:o,addEndListener:i,children:u,childRef:s,...a},m)=>{const h=y.useRef(null),f=mo(h,s),w=E=>{f(Hh(E))},g=E=>N=>{E&&h.current&&E(h.current,N)},x=y.useCallback(g(e),[e]),R=y.useCallback(g(t),[t]),p=y.useCallback(g(n),[n]),c=y.useCallback(g(r),[r]),v=y.useCallback(g(l),[l]),S=y.useCallback(g(o),[o]),T=y.useCallback(g(i),[i]);return d.jsx(zh,{ref:m,...a,onEnter:x,onEntered:p,onEntering:R,onExit:c,onExited:S,onExiting:v,addEndListener:T,nodeRef:h,children:typeof u=="function"?(E,N)=>u(E,{...N,ref:w}):Wt.cloneElement(u,{ref:w})})}),Vh=Wh;function Qh(e){const t=y.useRef(e);return y.useEffect(()=>{t.current=e},[e]),t}function Me(e){const t=Qh(e);return y.useCallback(function(...n){return t.current&&t.current(...n)},[t])}const Qf=e=>y.forwardRef((t,n)=>d.jsx("div",{...t,ref:n,className:M(t.className,e)})),Kf=Qf("h4");Kf.displayName="DivStyledAsH4";const Gf=y.forwardRef(({className:e,bsPrefix:t,as:n=Kf,...r},l)=>(t=H(t,"alert-heading"),d.jsx(n,{ref:l,className:M(e,t),...r})));Gf.displayName="AlertHeading";const Kh=Gf;function Gh(){return y.useState(null)}function Yh(){const e=y.useRef(!0),t=y.useRef(()=>e.current);return y.useEffect(()=>(e.current=!0,()=>{e.current=!1}),[]),t.current}function Xh(e){const t=y.useRef(null);return y.useEffect(()=>{t.current=e}),t.current}const Zh=typeof global<"u"&&global.navigator&&global.navigator.product==="ReactNative",Jh=typeof document<"u",ca=Jh||Zh?y.useLayoutEffect:y.useEffect,qh=["as","disabled"];function bh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function ev(e){return!e||e.trim()==="#"}function Hu({tagName:e,disabled:t,href:n,target:r,rel:l,role:o,onClick:i,tabIndex:u=0,type:s}){e||(n!=null||r!=null||l!=null?e="a":e="button");const a={tagName:e};if(e==="button")return[{type:s||"button",disabled:t},a];const m=f=>{if((t||e==="a"&&ev(n))&&f.preventDefault(),t){f.stopPropagation();return}i==null||i(f)},h=f=>{f.key===" "&&(f.preventDefault(),m(f))};return e==="a"&&(n||(n="#"),t&&(n=void 0)),[{role:o??"button",disabled:void 0,tabIndex:t?void 0:u,href:n,target:e==="a"?r:void 0,"aria-disabled":t||void 0,rel:e==="a"?l:void 0,onClick:m,onKeyDown:h},a]}const tv=y.forwardRef((e,t)=>{let{as:n,disabled:r}=e,l=bh(e,qh);const[o,{tagName:i}]=Hu(Object.assign({tagName:n,disabled:r},l));return d.jsx(i,Object.assign({},l,o,{ref:t}))});tv.displayName="Button";const nv=["onKeyDown"];function rv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function lv(e){return!e||e.trim()==="#"}const Yf=y.forwardRef((e,t)=>{let{onKeyDown:n}=e,r=rv(e,nv);const[l]=Hu(Object.assign({tagName:"a"},r)),o=Me(i=>{l.onKeyDown(i),n==null||n(i)});return lv(r.href)||r.role==="button"?d.jsx("a",Object.assign({ref:t},r,l,{onKeyDown:o})):d.jsx("a",Object.assign({ref:t},r,{onKeyDown:n}))});Yf.displayName="Anchor";const ov=Yf,Xf=y.forwardRef(({className:e,bsPrefix:t,as:n=ov,...r},l)=>(t=H(t,"alert-link"),d.jsx(n,{ref:l,className:M(e,t),...r})));Xf.displayName="AlertLink";const iv=Xf,uv={[St]:"show",[Ht]:"show"},Zf=y.forwardRef(({className:e,children:t,transitionClasses:n={},onEnter:r,...l},o)=>{const i={in:!1,timeout:300,mountOnEnter:!1,unmountOnExit:!1,appear:!1,...l},u=y.useCallback((s,a)=>{Uh(s),r==null||r(s,a)},[r]);return d.jsx(Vh,{ref:o,addEndListener:Ah,...i,onEnter:u,childRef:t.ref,children:(s,a)=>y.cloneElement(t,{...a,className:M("fade",e,t.props.className,uv[s],n[s])})})});Zf.displayName="Fade";const Kl=Zf,sv={"aria-label":it.string,onClick:it.func,variant:it.oneOf(["white"])},Wu=y.forwardRef(({className:e,variant:t,"aria-label":n="Close",...r},l)=>d.jsx("button",{ref:l,type:"button",className:M("btn-close",t&&`btn-close-${t}`,e),"aria-label":n,...r}));Wu.displayName="CloseButton";Wu.propTypes=sv;const Jf=Wu,qf=y.forwardRef((e,t)=>{const{bsPrefix:n,show:r=!0,closeLabel:l="Close alert",closeVariant:o,className:i,children:u,variant:s="primary",onClose:a,dismissible:m,transition:h=Kl,...f}=yh(e,{show:"onClose"}),w=H(n,"alert"),g=Me(p=>{a&&a(!1,p)}),x=h===!0?Kl:h,R=d.jsxs("div",{role:"alert",...x?void 0:f,ref:t,className:M(i,w,s&&`${w}-${s}`,m&&`${w}-dismissible`),children:[m&&d.jsx(Jf,{onClick:g,"aria-label":l,variant:o}),u]});return x?d.jsx(x,{unmountOnExit:!0,...f,ref:void 0,in:r,children:R}):r?R:null});qf.displayName="Alert";const fa=Object.assign(qf,{Link:iv,Heading:Kh}),bf=y.forwardRef(({as:e,bsPrefix:t,variant:n="primary",size:r,active:l=!1,disabled:o=!1,className:i,...u},s)=>{const a=H(t,"btn"),[m,{tagName:h}]=Hu({tagName:e,disabled:o,...u}),f=h;return d.jsx(f,{...m,...u,ref:s,disabled:o,className:M(i,a,l&&"active",n&&`${a}-${n}`,r&&`${a}-${r}`,u.href&&o&&"disabled")})});bf.displayName="Button";const tn=bf;function av(e){const t=y.useRef(e);return t.current=e,t}function ed(e){const t=av(e);y.useEffect(()=>()=>t.current(),[])}function cv(e,t){let n=0;return y.Children.map(e,r=>y.isValidElement(r)?t(r,n++):r)}function fv(e,t){return y.Children.toArray(e).some(n=>y.isValidElement(n)&&n.type===t)}function dv({as:e,bsPrefix:t,className:n,...r}){t=H(t,"col");const l=Df(),o=If(),i=[],u=[];return l.forEach(s=>{const a=r[s];delete r[s];let m,h,f;typeof a=="object"&&a!=null?{span:m,offset:h,order:f}=a:m=a;const w=s!==o?`-${s}`:"";m&&i.push(m===!0?`${t}${w}`:`${t}${w}-${m}`),f!=null&&u.push(`order${w}-${f}`),h!=null&&u.push(`offset${w}-${h}`)}),[{...r,className:M(n,...i,...u)},{as:e,bsPrefix:t,spans:i}]}const td=y.forwardRef((e,t)=>{const[{className:n,...r},{as:l="div",bsPrefix:o,spans:i}]=dv(e);return d.jsx(l,{...r,ref:t,className:M(n,!i.length&&o)})});td.displayName="Col";const Vu=td,nd=y.forwardRef(({bsPrefix:e,fluid:t=!1,as:n="div",className:r,...l},o)=>{const i=H(e,"container"),u=typeof t=="string"?`-${t}`:"-fluid";return d.jsx(n,{ref:o,...l,className:M(r,t?`${i}${u}`:i)})});nd.displayName="Container";const pv=nd;var mv=Function.prototype.bind.call(Function.prototype.call,[].slice);function dn(e,t){return mv(e.querySelectorAll(t))}function da(e,t){if(e.contains)return e.contains(t);if(e.compareDocumentPosition)return e===t||!!(e.compareDocumentPosition(t)&16)}const hv="data-rr-ui-";function vv(e){return`${hv}${e}`}const rd=y.createContext(Vn?window:void 0);rd.Provider;function Qu(){return y.useContext(rd)}const yv={type:it.string,tooltip:it.bool,as:it.elementType},Ku=y.forwardRef(({as:e="div",className:t,type:n="valid",tooltip:r=!1,...l},o)=>d.jsx(e,{...l,ref:o,className:M(t,`${n}-${r?"tooltip":"feedback"}`)}));Ku.displayName="Feedback";Ku.propTypes=yv;const ld=Ku,gv=y.createContext({}),ft=gv,od=y.forwardRef(({id:e,bsPrefix:t,className:n,type:r="checkbox",isValid:l=!1,isInvalid:o=!1,as:i="input",...u},s)=>{const{controlId:a}=y.useContext(ft);return t=H(t,"form-check-input"),d.jsx(i,{...u,ref:s,type:r,id:e||a,className:M(n,t,l&&"is-valid",o&&"is-invalid")})});od.displayName="FormCheckInput";const id=od,ud=y.forwardRef(({bsPrefix:e,className:t,htmlFor:n,...r},l)=>{const{controlId:o}=y.useContext(ft);return e=H(e,"form-check-label"),d.jsx("label",{...r,ref:l,htmlFor:n||o,className:M(t,e)})});ud.displayName="FormCheckLabel";const Gi=ud,sd=y.forwardRef(({id:e,bsPrefix:t,bsSwitchPrefix:n,inline:r=!1,reverse:l=!1,disabled:o=!1,isValid:i=!1,isInvalid:u=!1,feedbackTooltip:s=!1,feedback:a,feedbackType:m,className:h,style:f,title:w="",type:g="checkbox",label:x,children:R,as:p="input",...c},v)=>{t=H(t,"form-check"),n=H(n,"form-switch");const{controlId:S}=y.useContext(ft),T=y.useMemo(()=>({controlId:e||S}),[S,e]),E=!R&&x!=null&&x!==!1||fv(R,Gi),N=d.jsx(id,{...c,type:g==="switch"?"checkbox":g,ref:v,isValid:i,isInvalid:u,disabled:o,as:p});return d.jsx(ft.Provider,{value:T,children:d.jsx("div",{style:f,className:M(h,E&&t,r&&`${t}-inline`,l&&`${t}-reverse`,g==="switch"&&n),children:R||d.jsxs(d.Fragment,{children:[N,E&&d.jsx(Gi,{title:w,children:x}),a&&d.jsx(ld,{type:m,tooltip:s,children:a})]})})})});sd.displayName="FormCheck";const Gl=Object.assign(sd,{Input:id,Label:Gi}),ad=y.forwardRef(({bsPrefix:e,type:t,size:n,htmlSize:r,id:l,className:o,isValid:i=!1,isInvalid:u=!1,plaintext:s,readOnly:a,as:m="input",...h},f)=>{const{controlId:w}=y.useContext(ft);return e=H(e,"form-control"),d.jsx(m,{...h,type:t,size:r,ref:f,readOnly:a,id:l||w,className:M(o,s?`${e}-plaintext`:e,n&&`${e}-${n}`,t==="color"&&`${e}-color`,i&&"is-valid",u&&"is-invalid")})});ad.displayName="FormControl";const wv=Object.assign(ad,{Feedback:ld}),cd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"form-floating"),d.jsx(n,{ref:l,className:M(e,t),...r})));cd.displayName="FormFloating";const Sv=cd,fd=y.forwardRef(({controlId:e,as:t="div",...n},r)=>{const l=y.useMemo(()=>({controlId:e}),[e]);return d.jsx(ft.Provider,{value:l,children:d.jsx(t,{...n,ref:r})})});fd.displayName="FormGroup";const dd=fd,pd=y.forwardRef(({as:e="label",bsPrefix:t,column:n=!1,visuallyHidden:r=!1,className:l,htmlFor:o,...i},u)=>{const{controlId:s}=y.useContext(ft);t=H(t,"form-label");let a="col-form-label";typeof n=="string"&&(a=`${a} ${a}-${n}`);const m=M(l,t,r&&"visually-hidden",n&&a);return o=o||s,n?d.jsx(Vu,{ref:u,as:"label",className:m,htmlFor:o,...i}):d.jsx(e,{ref:u,className:m,htmlFor:o,...i})});pd.displayName="FormLabel";const xv=pd,md=y.forwardRef(({bsPrefix:e,className:t,id:n,...r},l)=>{const{controlId:o}=y.useContext(ft);return e=H(e,"form-range"),d.jsx("input",{...r,type:"range",ref:l,className:M(t,e),id:n||o})});md.displayName="FormRange";const kv=md,hd=y.forwardRef(({bsPrefix:e,size:t,htmlSize:n,className:r,isValid:l=!1,isInvalid:o=!1,id:i,...u},s)=>{const{controlId:a}=y.useContext(ft);return e=H(e,"form-select"),d.jsx("select",{...u,size:n,ref:s,className:M(r,e,t&&`${e}-${t}`,l&&"is-valid",o&&"is-invalid"),id:i||a})});hd.displayName="FormSelect";const Ev=hd,vd=y.forwardRef(({bsPrefix:e,className:t,as:n="small",muted:r,...l},o)=>(e=H(e,"form-text"),d.jsx(n,{...l,ref:o,className:M(t,e,r&&"text-muted")})));vd.displayName="FormText";const Cv=vd,yd=y.forwardRef((e,t)=>d.jsx(Gl,{...e,ref:t,type:"switch"}));yd.displayName="Switch";const Nv=Object.assign(yd,{Input:Gl.Input,Label:Gl.Label}),gd=y.forwardRef(({bsPrefix:e,className:t,children:n,controlId:r,label:l,...o},i)=>(e=H(e,"form-floating"),d.jsxs(dd,{ref:i,className:M(t,e),controlId:r,...o,children:[n,d.jsx("label",{htmlFor:r,children:l})]})));gd.displayName="FloatingLabel";const Tv=gd,_v={_ref:it.any,validated:it.bool,as:it.elementType},Gu=y.forwardRef(({className:e,validated:t,as:n="form",...r},l)=>d.jsx(n,{...r,ref:l,className:M(e,t&&"was-validated")}));Gu.displayName="Form";Gu.propTypes=_v;const pe=Object.assign(Gu,{Group:dd,Control:wv,Floating:Sv,Check:Gl,Switch:Nv,Label:xv,Text:Cv,Range:kv,Select:Ev,FloatingLabel:Tv});var ul;function pa(e){if((!ul&&ul!==0||e)&&Vn){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),ul=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return ul}function Wo(e){e===void 0&&(e=po());try{var t=e.activeElement;return!t||!t.nodeName?null:t}catch{return e.body}}function jv(e=document){const t=e.defaultView;return Math.abs(t.innerWidth-e.documentElement.clientWidth)}const ma=vv("modal-open");class Rv{constructor({ownerDocument:t,handleContainerOverflow:n=!0,isRTL:r=!1}={}){this.handleContainerOverflow=n,this.isRTL=r,this.modals=[],this.ownerDocument=t}getScrollbarWidth(){return jv(this.ownerDocument)}getElement(){return(this.ownerDocument||document).body}setModalAttributes(t){}removeModalAttributes(t){}setContainerStyle(t){const n={overflow:"hidden"},r=this.isRTL?"paddingLeft":"paddingRight",l=this.getElement();t.style={overflow:l.style.overflow,[r]:l.style[r]},t.scrollBarWidth&&(n[r]=`${parseInt(Xt(l,r)||"0",10)+t.scrollBarWidth}px`),l.setAttribute(ma,""),Xt(l,n)}reset(){[...this.modals].forEach(t=>this.remove(t))}removeContainerStyle(t){const n=this.getElement();n.removeAttribute(ma),Object.assign(n.style,t.style)}add(t){let n=this.modals.indexOf(t);return n!==-1||(n=this.modals.length,this.modals.push(t),this.setModalAttributes(t),n!==0)||(this.state={scrollBarWidth:this.getScrollbarWidth(),style:{}},this.handleContainerOverflow&&this.setContainerStyle(this.state)),n}remove(t){const n=this.modals.indexOf(t);n!==-1&&(this.modals.splice(n,1),!this.modals.length&&this.handleContainerOverflow&&this.removeContainerStyle(this.state),this.removeModalAttributes(t))}isTopModal(t){return!!this.modals.length&&this.modals[this.modals.length-1]===t}}const Yu=Rv,Vo=(e,t)=>Vn?e==null?(t||po()).body:(typeof e=="function"&&(e=e()),e&&"current"in e&&(e=e.current),e&&("nodeType"in e||e.getBoundingClientRect)?e:null):null;function Lv(e,t){const n=Qu(),[r,l]=y.useState(()=>Vo(e,n==null?void 0:n.document));if(!r){const o=Vo(e);o&&l(o)}return y.useEffect(()=>{t&&r&&t(r)},[t,r]),y.useEffect(()=>{const o=Vo(e);o!==r&&l(o)},[e,r]),r}function Ov({children:e,in:t,onExited:n,mountOnEnter:r,unmountOnExit:l}){const o=y.useRef(null),i=y.useRef(t),u=Me(n);y.useEffect(()=>{t?i.current=!0:u(o.current)},[t,u]);const s=mo(o,e.ref),a=y.cloneElement(e,{ref:s});return t?a:l||!i.current&&r?null:a}function Pv({in:e,onTransition:t}){const n=y.useRef(null),r=y.useRef(!0),l=Me(t);return ca(()=>{if(!n.current)return;let o=!1;return l({in:e,element:n.current,initial:r.current,isStale:()=>o}),()=>{o=!0}},[e,l]),ca(()=>(r.current=!1,()=>{r.current=!0}),[]),n}function Fv({children:e,in:t,onExited:n,onEntered:r,transition:l}){const[o,i]=y.useState(!t);t&&o&&i(!1);const u=Pv({in:!!t,onTransition:a=>{const m=()=>{a.isStale()||(a.in?r==null||r(a.element,a.initial):(i(!0),n==null||n(a.element)))};Promise.resolve(l(a)).then(m,h=>{throw a.in||i(!0),h})}}),s=mo(u,e.ref);return o&&!t?null:y.cloneElement(e,{ref:s})}function ha(e,t,n){return e?d.jsx(e,Object.assign({},n)):t?d.jsx(Fv,Object.assign({},n,{transition:t})):d.jsx(Ov,Object.assign({},n))}function Mv(e){return e.code==="Escape"||e.keyCode===27}const zv=["show","role","className","style","children","backdrop","keyboard","onBackdropClick","onEscapeKeyDown","transition","runTransition","backdropTransition","runBackdropTransition","autoFocus","enforceFocus","restoreFocus","restoreFocusOptions","renderDialog","renderBackdrop","manager","container","onShow","onHide","onExit","onExited","onExiting","onEnter","onEntering","onEntered"];function $v(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}let Qo;function Dv(e){return Qo||(Qo=new Yu({ownerDocument:e==null?void 0:e.document})),Qo}function Iv(e){const t=Qu(),n=e||Dv(t),r=y.useRef({dialog:null,backdrop:null});return Object.assign(r.current,{add:()=>n.add(r.current),remove:()=>n.remove(r.current),isTopModal:()=>n.isTopModal(r.current),setDialogRef:y.useCallback(l=>{r.current.dialog=l},[]),setBackdropRef:y.useCallback(l=>{r.current.backdrop=l},[])})}const wd=y.forwardRef((e,t)=>{let{show:n=!1,role:r="dialog",className:l,style:o,children:i,backdrop:u=!0,keyboard:s=!0,onBackdropClick:a,onEscapeKeyDown:m,transition:h,runTransition:f,backdropTransition:w,runBackdropTransition:g,autoFocus:x=!0,enforceFocus:R=!0,restoreFocus:p=!0,restoreFocusOptions:c,renderDialog:v,renderBackdrop:S=K=>d.jsx("div",Object.assign({},K)),manager:T,container:E,onShow:N,onHide:j=()=>{},onExit:U,onExited:P,onExiting:ie,onEnter:Ke,onEntering:Ge,onEntered:on}=e,Qn=$v(e,zv);const Re=Qu(),Ye=Lv(E),C=Iv(T),L=Yh(),O=Xh(n),[D,A]=y.useState(!n),fe=y.useRef(null);y.useImperativeHandle(t,()=>C,[C]),Vn&&!O&&n&&(fe.current=Wo(Re==null?void 0:Re.document)),n&&D&&A(!1);const Le=Me(()=>{if(C.add(),sn.current=Ql(document,"keydown",ho),un.current=Ql(document,"focus",()=>setTimeout(Oe),!0),N&&N(),x){var K,Hr;const Yn=Wo((K=(Hr=C.dialog)==null?void 0:Hr.ownerDocument)!=null?K:Re==null?void 0:Re.document);C.dialog&&Yn&&!da(C.dialog,Yn)&&(fe.current=Yn,C.dialog.focus())}}),et=Me(()=>{if(C.remove(),sn.current==null||sn.current(),un.current==null||un.current(),p){var K;(K=fe.current)==null||K.focus==null||K.focus(c),fe.current=null}});y.useEffect(()=>{!n||!Ye||Le()},[n,Ye,Le]),y.useEffect(()=>{D&&et()},[D,et]),ed(()=>{et()});const Oe=Me(()=>{if(!R||!L()||!C.isTopModal())return;const K=Wo(Re==null?void 0:Re.document);C.dialog&&K&&!da(C.dialog,K)&&C.dialog.focus()}),mt=Me(K=>{K.target===K.currentTarget&&(a==null||a(K),u===!0&&j())}),ho=Me(K=>{s&&Mv(K)&&C.isTopModal()&&(m==null||m(K),K.defaultPrevented||j())}),un=y.useRef(),sn=y.useRef(),Kn=(...K)=>{A(!0),P==null||P(...K)};if(!Ye)return null;const Br=Object.assign({role:r,ref:C.setDialogRef,"aria-modal":r==="dialog"?!0:void 0},Qn,{style:o,className:l,tabIndex:-1});let Gn=v?v(Br):d.jsx("div",Object.assign({},Br,{children:y.cloneElement(i,{role:"document"})}));Gn=ha(h,f,{unmountOnExit:!0,mountOnEnter:!0,appear:!0,in:!!n,onExit:U,onExiting:ie,onExited:Kn,onEnter:Ke,onEntering:Ge,onEntered:on,children:Gn});let It=null;return u&&(It=S({ref:C.setBackdropRef,onClick:mt}),It=ha(w,g,{in:!!n,appear:!0,mountOnEnter:!0,unmountOnExit:!0,children:It})),d.jsx(d.Fragment,{children:Tn.createPortal(d.jsxs(d.Fragment,{children:[It,Gn]}),Ye)})});wd.displayName="Modal";const Av=Object.assign(wd,{Manager:Yu});function Uv(e,t){return e.classList?!!t&&e.classList.contains(t):(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")!==-1}function Bv(e,t){e.classList?e.classList.add(t):Uv(e,t)||(typeof e.className=="string"?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))}function va(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function Hv(e,t){e.classList?e.classList.remove(t):typeof e.className=="string"?e.className=va(e.className,t):e.setAttribute("class",va(e.className&&e.className.baseVal||"",t))}const pn={FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"};class Wv extends Yu{adjustAndStore(t,n,r){const l=n.style[t];n.dataset[t]=l,Xt(n,{[t]:`${parseFloat(Xt(n,t))+r}px`})}restore(t,n){const r=n.dataset[t];r!==void 0&&(delete n.dataset[t],Xt(n,{[t]:r}))}setContainerStyle(t){super.setContainerStyle(t);const n=this.getElement();if(Bv(n,"modal-open"),!t.scrollBarWidth)return;const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";dn(n,pn.FIXED_CONTENT).forEach(o=>this.adjustAndStore(r,o,t.scrollBarWidth)),dn(n,pn.STICKY_CONTENT).forEach(o=>this.adjustAndStore(l,o,-t.scrollBarWidth)),dn(n,pn.NAVBAR_TOGGLER).forEach(o=>this.adjustAndStore(l,o,t.scrollBarWidth))}removeContainerStyle(t){super.removeContainerStyle(t);const n=this.getElement();Hv(n,"modal-open");const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";dn(n,pn.FIXED_CONTENT).forEach(o=>this.restore(r,o)),dn(n,pn.STICKY_CONTENT).forEach(o=>this.restore(l,o)),dn(n,pn.NAVBAR_TOGGLER).forEach(o=>this.restore(l,o))}}let Ko;function Vv(e){return Ko||(Ko=new Wv(e)),Ko}const Sd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-body"),d.jsx(n,{ref:l,className:M(e,t),...r})));Sd.displayName="ModalBody";const Qv=Sd,Kv=y.createContext({onHide(){}}),xd=Kv,kd=y.forwardRef(({bsPrefix:e,className:t,contentClassName:n,centered:r,size:l,fullscreen:o,children:i,scrollable:u,...s},a)=>{e=H(e,"modal");const m=`${e}-dialog`,h=typeof o=="string"?`${e}-fullscreen-${o}`:`${e}-fullscreen`;return d.jsx("div",{...s,ref:a,className:M(m,t,l&&`${e}-${l}`,r&&`${m}-centered`,u&&`${m}-scrollable`,o&&h),children:d.jsx("div",{className:M(`${e}-content`,n),children:i})})});kd.displayName="ModalDialog";const Ed=kd,Cd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-footer"),d.jsx(n,{ref:l,className:M(e,t),...r})));Cd.displayName="ModalFooter";const Gv=Cd,Yv=y.forwardRef(({closeLabel:e="Close",closeVariant:t,closeButton:n=!1,onHide:r,children:l,...o},i)=>{const u=y.useContext(xd),s=Me(()=>{u==null||u.onHide(),r==null||r()});return d.jsxs("div",{ref:i,...o,children:[l,n&&d.jsx(Jf,{"aria-label":e,variant:t,onClick:s})]})}),Xv=Yv,Nd=y.forwardRef(({bsPrefix:e,className:t,closeLabel:n="Close",closeButton:r=!1,...l},o)=>(e=H(e,"modal-header"),d.jsx(Xv,{ref:o,...l,className:M(t,e),closeLabel:n,closeButton:r})));Nd.displayName="ModalHeader";const Zv=Nd,Jv=Qf("h4"),Td=y.forwardRef(({className:e,bsPrefix:t,as:n=Jv,...r},l)=>(t=H(t,"modal-title"),d.jsx(n,{ref:l,className:M(e,t),...r})));Td.displayName="ModalTitle";const qv=Td;function bv(e){return d.jsx(Kl,{...e,timeout:null})}function ey(e){return d.jsx(Kl,{...e,timeout:null})}const _d=y.forwardRef(({bsPrefix:e,className:t,style:n,dialogClassName:r,contentClassName:l,children:o,dialogAs:i=Ed,"aria-labelledby":u,"aria-describedby":s,"aria-label":a,show:m=!1,animation:h=!0,backdrop:f=!0,keyboard:w=!0,onEscapeKeyDown:g,onShow:x,onHide:R,container:p,autoFocus:c=!0,enforceFocus:v=!0,restoreFocus:S=!0,restoreFocusOptions:T,onEntered:E,onExit:N,onExiting:j,onEnter:U,onEntering:P,onExited:ie,backdropClassName:Ke,manager:Ge,...on},Qn)=>{const[Re,Ye]=y.useState({}),[C,L]=y.useState(!1),O=y.useRef(!1),D=y.useRef(!1),A=y.useRef(null),[fe,Le]=Gh(),et=mo(Qn,Le),Oe=Me(R),mt=xh();e=H(e,"modal");const ho=y.useMemo(()=>({onHide:Oe}),[Oe]);function un(){return Ge||Vv({isRTL:mt})}function sn($){if(!Vn)return;const an=un().getScrollbarWidth()>0,Zu=$.scrollHeight>po($).documentElement.clientHeight;Ye({paddingRight:an&&!Zu?pa():void 0,paddingLeft:!an&&Zu?pa():void 0})}const Kn=Me(()=>{fe&&sn(fe.dialog)});ed(()=>{Ki(window,"resize",Kn),A.current==null||A.current()});const Br=()=>{O.current=!0},Gn=$=>{O.current&&fe&&$.target===fe.dialog&&(D.current=!0),O.current=!1},It=()=>{L(!0),A.current=Vf(fe.dialog,()=>{L(!1)})},K=$=>{$.target===$.currentTarget&&It()},Hr=$=>{if(f==="static"){K($);return}if(D.current||$.target!==$.currentTarget){D.current=!1;return}R==null||R()},Yn=$=>{w?g==null||g($):($.preventDefault(),f==="static"&&It())},Dd=($,an)=>{$&&sn($),U==null||U($,an)},Id=$=>{A.current==null||A.current(),N==null||N($)},Ad=($,an)=>{P==null||P($,an),Wf(window,"resize",Kn)},Ud=$=>{$&&($.style.display=""),ie==null||ie($),Ki(window,"resize",Kn)},Bd=y.useCallback($=>d.jsx("div",{...$,className:M(`${e}-backdrop`,Ke,!h&&"show")}),[h,Ke,e]),Xu={...n,...Re};Xu.display="block";const Hd=$=>d.jsx("div",{role:"dialog",...$,style:Xu,className:M(t,e,C&&`${e}-static`,!h&&"show"),onClick:f?Hr:void 0,onMouseUp:Gn,"aria-label":a,"aria-labelledby":u,"aria-describedby":s,children:d.jsx(i,{...on,onMouseDown:Br,className:r,contentClassName:l,children:o})});return d.jsx(xd.Provider,{value:ho,children:d.jsx(Av,{show:m,ref:et,backdrop:f,container:p,keyboard:!0,autoFocus:c,enforceFocus:v,restoreFocus:S,restoreFocusOptions:T,onEscapeKeyDown:Yn,onShow:x,onHide:R,onEnter:Dd,onEntering:Ad,onEntered:E,onExit:Id,onExiting:j,onExited:Ud,manager:un(),transition:h?bv:void 0,backdropTransition:h?ey:void 0,renderBackdrop:Bd,renderDialog:Hd})})});_d.displayName="Modal";const ge=Object.assign(_d,{Body:Qv,Header:Zv,Title:qv,Footer:Gv,Dialog:Ed,TRANSITION_DURATION:300,BACKDROP_TRANSITION_DURATION:150}),ya=1e3;function ty(e,t,n){const r=(e-t)/(n-t)*100;return Math.round(r*ya)/ya}function ga({min:e,now:t,max:n,label:r,visuallyHidden:l,striped:o,animated:i,className:u,style:s,variant:a,bsPrefix:m,...h},f){return d.jsx("div",{ref:f,...h,role:"progressbar",className:M(u,`${m}-bar`,{[`bg-${a}`]:a,[`${m}-bar-animated`]:i,[`${m}-bar-striped`]:i||o}),style:{width:`${ty(t,e,n)}%`,...s},"aria-valuenow":t,"aria-valuemin":e,"aria-valuemax":n,children:l?d.jsx("span",{className:"visually-hidden",children:r}):r})}const jd=y.forwardRef(({isChild:e=!1,...t},n)=>{const r={min:0,max:100,animated:!1,visuallyHidden:!1,striped:!1,...t};if(r.bsPrefix=H(r.bsPrefix,"progress"),e)return ga(r,n);const{min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:m,bsPrefix:h,variant:f,className:w,children:g,...x}=r;return d.jsx("div",{ref:n,...x,className:M(w,h),children:g?cv(g,R=>y.cloneElement(R,{isChild:!0})):ga({min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:m,bsPrefix:h,variant:f},n)})});jd.displayName="ProgressBar";const ny=jd,Rd=y.forwardRef(({bsPrefix:e,className:t,as:n="div",...r},l)=>{const o=H(e,"row"),i=Df(),u=If(),s=`${o}-cols`,a=[];return i.forEach(m=>{const h=r[m];delete r[m];let f;h!=null&&typeof h=="object"?{cols:f}=h:f=h;const w=m!==u?`-${m}`:"";f!=null&&a.push(`${s}${w}-${f}`)}),d.jsx(n,{ref:l,...r,className:M(t,o,...a)})});Rd.displayName="Row";const Ld=Rd,Od=y.forwardRef(({bsPrefix:e,variant:t,animation:n="border",size:r,as:l="div",className:o,...i},u)=>{e=H(e,"spinner");const s=`${e}-${n}`;return d.jsx(l,{ref:u,...i,className:M(o,s,r&&`${s}-${r}`,t&&`text-${t}`)})});Od.displayName="Spinner";const Un=Od,Sl="initializing",wa="paused",Pd="live",ry="error",ln=y.createContext({listTorrents:()=>{throw new Error("Function not implemented.")},getTorrentDetails:()=>{throw new Error("Function not implemented.")},getTorrentStats:()=>{throw new Error("Function not implemented.")},uploadTorrent:()=>{throw new Error("Function not implemented.")},pause:()=>{throw new Error("Function not implemented.")},start:()=>{throw new Error("Function not implemented.")},forget:()=>{throw new Error("Function not implemented.")},delete:()=>{throw new Error("Function not implemented.")}}),Ur=y.createContext({setCloseableError:e=>{},refreshTorrents:()=>{}}),Fd=y.createContext({refresh:()=>{}}),Go=({className:e,onClick:t,disabled:n,color:r})=>{const l=o=>{o.stopPropagation(),!n&&t()};return d.jsx("a",{className:`bi ${e} p-1`,onClick:l,href:"#"})},ly=({id:e,show:t,onHide:n})=>{if(!t)return null;const[r,l]=y.useState(!1),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(Ur),m=y.useContext(ln),h=()=>{l(!1),i(null),s(!1),n()},f=()=>{s(!0),(r?m.delete:m.forget)(e).then(()=>{a.refreshTorrents(),h()}).catch(g=>{i({text:`Error deleting torrent id=${e}`,details:g}),s(!1)})};return d.jsxs(ge,{show:t,onHide:h,children:[d.jsx(ge.Header,{closeButton:!0,children:"Delete torrent"}),d.jsxs(ge.Body,{children:[d.jsx(pe,{children:d.jsx(pe.Group,{controlId:"delete-torrent",children:d.jsx(pe.Check,{type:"checkbox",label:"Also delete files",checked:r,onChange:()=>l(!r)})})}),o&&d.jsx(Mr,{error:o})]}),d.jsxs(ge.Footer,{children:[u&&d.jsx(Un,{}),d.jsx(tn,{variant:"primary",onClick:f,disabled:u,children:"OK"}),d.jsx(tn,{variant:"secondary",onClick:h,children:"Cancel"})]})]})},oy=({id:e,statsResponse:t})=>{let n=t.state,[r,l]=y.useState(!1),[o,i]=y.useState(!1),u=y.useContext(Fd);const s=n=="live",a=n=="paused"||n=="error",m=y.useContext(Ur),h=y.useContext(ln),f=()=>{l(!0),h.start(e).then(()=>{u.refresh()},R=>{m.setCloseableError({text:`Error starting torrent id=${e}`,details:R})}).finally(()=>l(!1))},w=()=>{l(!0),h.pause(e).then(()=>{u.refresh()},R=>{m.setCloseableError({text:`Error pausing torrent id=${e}`,details:R})}).finally(()=>l(!1))},g=()=>{l(!0),i(!0)},x=()=>{l(!1),i(!1)};return d.jsx(Ld,{children:d.jsxs(Vu,{children:[a&&d.jsx(Go,{className:"bi-play-circle",onClick:f,disabled:r,color:"success"}),s&&d.jsx(Go,{className:"bi-pause-circle",onClick:w,disabled:r}),d.jsx(Go,{className:"bi-x-circle",onClick:g,disabled:r,color:"danger"}),d.jsx(ly,{id:e,show:o,onHide:x})]})})},iy=({id:e,detailsResponse:t,statsResponse:n})=>{const r=(n==null?void 0:n.state)??"",l=n==null?void 0:n.error,o=(n==null?void 0:n.total_bytes)??1,i=(n==null?void 0:n.progress_bytes)??0,u=(n==null?void 0:n.finished)||!1,s=l?100:i/o*100,a=(r==Sl||r==Pd)&&!u,m=l?"Error":`${s.toFixed(2)}%`,h=l?"danger":u?"success":r==Sl?"warning":"primary",f=()=>{var R;let x=(R=n==null?void 0:n.live)==null?void 0:R.snapshot.peer_stats;return x?`${x.live} / ${x.seen}`:""},w=()=>{var x;if(u)return"Completed";switch(r){case wa:return"Paused";case Sl:return"Checking files";case ry:return"Error"}return((x=n==null?void 0:n.live)==null?void 0:x.download_speed.human_readable)??"N/A"};let g=[];return l?g.push("bg-warning"):e%2==0&&g.push("bg-light"),d.jsxs(Ld,{className:g.join(" "),children:[d.jsx(vt,{size:3,label:"Name",children:t?d.jsxs(d.Fragment,{children:[d.jsx("div",{className:"text-truncate",children:yy(t)}),l&&d.jsxs("p",{className:"text-danger",children:[d.jsx("strong",{children:"Error:"})," ",l]})]}):d.jsx(Un,{})}),n?d.jsxs(d.Fragment,{children:[d.jsx(vt,{label:"Size",children:`${zd(o)} `}),d.jsx(vt,{size:2,label:(r==wa,"Progress"),children:d.jsx(ny,{now:s,label:m,animated:a,variant:h})}),d.jsx(vt,{size:2,label:"Down Speed",children:w()}),d.jsx(vt,{label:"ETA",children:gy(n)}),d.jsx(vt,{size:2,label:"Peers",children:f()}),d.jsx(vt,{label:"Actions",children:d.jsx(oy,{id:e,statsResponse:n})})]}):d.jsx(vt,{label:"Loading stats",size:8,children:d.jsx(Un,{})})]})},vt=({size:e,label:t,children:n})=>d.jsxs(Vu,{md:e||1,className:"py-3",children:[d.jsx("div",{className:"fw-bold",children:t}),n]}),uy=({id:e,torrent:t})=>{const[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(0),s=y.useContext(ln),a=()=>{u(i+1)};return y.useEffect(()=>{if(n===null)return Sy(async()=>{await s.getTorrentDetails(t.id).then(r)},1e3)},[n]),y.useEffect(()=>$d(async()=>s.getTorrentStats(t.id).then(g=>(o(g),g)).then(g=>g.finished?1e4:g.state==Sl||g.state==Pd?1e3:1e4,()=>1e4),0),[i]),d.jsx(Fd.Provider,{value:{refresh:a},children:d.jsx(iy,{id:e,detailsResponse:n,statsResponse:l})})},sy=e=>{if(e.torrents===null&&e.loading)return d.jsx(Un,{});if(e.torrents!==null)return e.torrents.length===0?d.jsx("div",{className:"text-center",children:d.jsx("p",{children:"No existing torrents found. Add them through buttons below."})}):d.jsx("div",{style:{fontSize:"smaller"},children:e.torrents.map(t=>d.jsx(uy,{id:t.id,torrent:t},t.id))})},ay=e=>{const[t,n]=y.useState(null),[r,l]=y.useState(null),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(ln),m=async()=>{s(!0);let f=await a.listTorrents().finally(()=>s(!1));i(f.torrents)};y.useEffect(()=>$d(async()=>m().then(()=>(l(null),5e3),f=>(l({text:"Error refreshing torrents",details:f}),console.error(f),5e3)),0),[]);const h={setCloseableError:n,refreshTorrents:m};return d.jsx(Ur.Provider,{value:h,children:d.jsxs("div",{className:"text-center",children:[d.jsx("h1",{className:"mt-3 mb-4",children:e.title}),d.jsx(vy,{closeableError:t,otherError:r,torrents:o,torrentsLoading:u})]})})},cy=e=>{let{details:t}=e;return t?d.jsxs(d.Fragment,{children:[t.statusText&&d.jsx("p",{children:d.jsx("strong",{children:t.statusText})}),d.jsx("pre",{children:t.text})]}):null},Mr=e=>{let{error:t,remove:n}=e;return t==null?null:d.jsxs(fa,{variant:"danger",onClose:n,dismissible:n!=null,children:[d.jsx(fa.Heading,{children:t.text}),d.jsx(cy,{details:t.details})]})},Md=({buttonText:e,onClick:t,data:n,resetData:r,variant:l})=>{const[o,i]=y.useState(!1),[u,s]=y.useState(null),[a,m]=y.useState(null),h=y.useContext(ln);y.useEffect(()=>{if(n===null)return;let w=setTimeout(async()=>{i(!0);try{const g=await h.uploadTorrent(n,{list_only:!0});s(g)}catch(g){m({text:"Error listing torrent files",details:g})}finally{i(!1)}},0);return()=>clearTimeout(w)},[n]);const f=()=>{r(),m(null),s(null),i(!1)};return d.jsxs(d.Fragment,{children:[d.jsx(tn,{variant:l,onClick:t,className:"m-1",children:e}),n&&d.jsx(my,{onHide:f,listTorrentError:a,listTorrentResponse:u,data:n,listTorrentLoading:o})]})},fy=({show:e,setUrl:t,cancel:n})=>{let[r,l]=y.useState("");return d.jsxs(ge,{show:e,onHide:n,size:"lg",children:[d.jsx(ge.Header,{closeButton:!0,children:d.jsx(ge.Title,{children:"Add torrent"})}),d.jsx(ge.Body,{children:d.jsx(pe,{children:d.jsxs(pe.Group,{className:"mb-3",controlId:"url",children:[d.jsx(pe.Label,{children:"Enter magnet or HTTP(S) URL to the .torrent"}),d.jsx(pe.Control,{value:r,placeholder:"magnet:?xt=urn:btih:...",onChange:o=>{l(o.target.value)}})]})})}),d.jsxs(ge.Footer,{children:[d.jsx(tn,{variant:"primary",onClick:()=>{t(r),l("")},disabled:r.length==0,children:"OK"}),d.jsx(tn,{variant:"secondary",onClick:n,children:"Cancel"})]})]})},dy=()=>{let[e,t]=y.useState(null),[n,r]=y.useState(!1);return d.jsxs(d.Fragment,{children:[d.jsx(Md,{variant:"primary",buttonText:"Add Torrent from Magnet / URL",onClick:()=>{r(!0)},data:e,resetData:()=>t(null)}),d.jsx(fy,{show:n,setUrl:l=>{r(!1),t(l)},cancel:()=>{r(!1),t(null)}})]})},py=()=>{const e=y.useRef(),[t,n]=y.useState(null),r=async()=>{var u;if(!((u=e==null?void 0:e.current)!=null&&u.files))return;const i=e.current.files[0];n(i)},l=()=>{e!=null&&e.current&&(e.current.value="",n(null))},o=()=>{e!=null&&e.current&&e.current.click()};return d.jsxs(d.Fragment,{children:[d.jsx("input",{type:"file",ref:e,accept:".torrent",onChange:r,className:"d-none"}),d.jsx(Md,{variant:"secondary",buttonText:"Upload .torrent File",onClick:o,data:t,resetData:l})]})},my=e=>{let{onHide:t,listTorrentResponse:n,listTorrentError:r,listTorrentLoading:l,data:o}=e;const[i,u]=y.useState([]),[s,a]=y.useState(!1),[m,h]=y.useState(null),[f,w]=y.useState(!1),[g,x]=y.useState(""),R=y.useContext(Ur),p=y.useContext(ln);y.useEffect(()=>{console.log(n),u(n?n.details.files.map((E,N)=>N):[]),x((n==null?void 0:n.output_folder)||"")},[n]);const c=()=>{t(),u([]),h(null),a(!1)},v=E=>{i.includes(E)?u(i.filter(N=>N!==E)):u([...i,E])},S=async()=>{if(!n)return;a(!0);let E=n.seen_peers?n.seen_peers.slice(0,32):null,N={overwrite:!0,only_files:i,initial_peers:E,output_folder:g};f&&(N.peer_opts={connect_timeout:20,read_write_timeout:60}),p.uploadTorrent(o,N).then(()=>{t(),R.refreshTorrents()},j=>{h({text:"Error starting torrent",details:j})}).finally(()=>a(!1))},T=()=>{if(l)return d.jsx(Un,{});if(r)return d.jsx(Mr,{error:r});if(n)return d.jsxs(pe,{children:[d.jsxs("fieldset",{className:"mb-4",children:[d.jsx("legend",{children:"Pick the files to download"}),n.details.files.map((E,N)=>d.jsx(pe.Group,{controlId:`check-${N}`,children:d.jsx(pe.Check,{type:"checkbox",label:`${E.name} (${zd(E.length)})`,checked:i.includes(N),onChange:()=>v(N)})},N))]}),d.jsxs("fieldset",{children:[d.jsx("legend",{children:"Options"}),d.jsxs(pe.Group,{controlId:"output-folder",className:"mb-3",children:[d.jsx(pe.Label,{children:"Output folder"}),d.jsx(pe.Control,{type:"text",value:g,onChange:E=>x(E.target.value)})]}),d.jsxs(pe.Group,{controlId:"unpopular-torrent",className:"mb-3",children:[d.jsx(pe.Check,{type:"checkbox",label:"Increase timeouts",checked:f,onChange:()=>w(!f)}),d.jsx("small",{id:"emailHelp",className:"form-text text-muted",children:"This might be useful for unpopular torrents with few peers. It will slow down fast torrents though."})]})]})]})};return d.jsxs(ge,{show:!0,onHide:c,size:"lg",children:[d.jsx(ge.Header,{closeButton:!0,children:d.jsx(ge.Title,{children:"Add torrent"})}),d.jsxs(ge.Body,{children:[T(),d.jsx(Mr,{error:m})]}),d.jsxs(ge.Footer,{children:[s&&d.jsx(Un,{}),d.jsx(tn,{variant:"primary",onClick:S,disabled:l||s||i.length==0,children:"OK"}),d.jsx(tn,{variant:"secondary",onClick:c,children:"Cancel"})]})]})},hy=()=>d.jsxs("div",{id:"buttons-container",className:"mt-3",children:[d.jsx(dy,{}),d.jsx(py,{})]}),vy=e=>{let t=y.useContext(Ur);return d.jsxs(pv,{children:[d.jsx(Mr,{error:e.closeableError,remove:()=>t.setCloseableError(null)}),d.jsx(Mr,{error:e.otherError}),d.jsx(sy,{torrents:e.torrents,loading:e.torrentsLoading}),d.jsx(hy,{})]})};function zd(e){if(e===0)return"0 Bytes";const t=1024,n=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,r)).toFixed(2))+" "+n[r]}function yy(e){return e.files.filter(n=>n.included).reduce((n,r)=>n.length>r.length?n:r).name}function gy(e){var n,r,l;let t=(l=(r=(n=e==null?void 0:e.live)==null?void 0:n.time_remaining)==null?void 0:r.duration)==null?void 0:l.secs;return t==null?"N/A":wy(t)}function wy(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,l=(o,i)=>o>0?`${o}${i}`:"";return t>0?`${l(t,"h")} ${l(n,"m")}`.trim():n>0?`${l(n,"m")} ${l(r,"s")}`.trim():`${l(r,"s")}`.trim()}function $d(e,t){let n,r=t;const l=async()=>{if(r=await e(),r==null)throw"asyncCallback returned null or undefined";o()};let o=()=>{n=setTimeout(l,r)};return o(),()=>{clearTimeout(n)}}function Sy(e,t){let n;const r=async()=>{await e().then(()=>!1,()=>!0)&&l()};let l=o=>{n=setTimeout(r,o!==void 0?o:t)};return l(0),()=>clearTimeout(n)}const xy=window.origin==="null"||window.origin==="http://localhost:3031"?"http://localhost:3030":"",yt=async(e,t,n)=>{console.log(e,t);const r=xy+t,l={method:e,headers:{Accept:"application/json"},body:n};let o={method:e,path:t,text:""},i;try{i=await fetch(r,l)}catch{return o.text="network error",Promise.reject(o)}if(o.status=i.status,o.statusText=`${i.status} ${i.statusText}`,!i.ok){const s=await i.text();try{const a=JSON.parse(s);o.text=a.human_readable!==void 0?a.human_readable:JSON.stringify(a,null,2)}catch{o.text=s}return Promise.reject(o)}return await i.json()},ky={listTorrents:()=>yt("GET","/torrents"),getTorrentDetails:e=>yt("GET",`/torrents/${e}`),getTorrentStats:e=>yt("GET",`/torrents/${e}/stats/v1`),uploadTorrent:(e,t)=>{var r,l;let n="/torrents?&overwrite=true";return t!=null&&t.list_only&&(n+="&list_only=true"),(t==null?void 0:t.only_files)!=null&&(n+=`&only_files=${t.only_files.join(",")}`),(r=t==null?void 0:t.peer_opts)!=null&&r.connect_timeout&&(n+=`&peer_connect_timeout=${t.peer_opts.connect_timeout}`),(l=t==null?void 0:t.peer_opts)!=null&&l.read_write_timeout&&(n+=`&peer_read_write_timeout=${t.peer_opts.read_write_timeout}`),t!=null&&t.initial_peers&&(n+=`&initial_peers=${t.initial_peers.join(",")}`),t!=null&&t.output_folder&&(n+=`&output_folder=${t.output_folder}`),typeof e=="string"&&(n+="&is_url=true"),yt("POST",n,e)},pause:e=>yt("POST",`/torrents/${e}/pause`),start:e=>yt("POST",`/torrents/${e}/start`),forget:e=>yt("POST",`/torrents/${e}/forget`),delete:e=>yt("POST",`/torrents/${e}/delete`)};Yo.createRoot(document.getElementById("app")).render(d.jsx(y.StrictMode,{children:d.jsx(ln.Provider,{value:ky,children:d.jsx(ay,{title:"rqbit web UI - version 4.0.0-beta.3"})})})); +*/(function(e){(function(){var t={}.hasOwnProperty;function n(){for(var r=[],l=0;l=0)&&(n[l]=e[l]);return n}function ia(e){return"default"+e.charAt(0).toUpperCase()+e.substr(1)}function mh(e){var t=hh(e,"string");return typeof t=="symbol"?t:String(t)}function hh(e,t){if(typeof e!="object"||e===null)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||"default");if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function vh(e,t,n){var r=y.useRef(e!==void 0),l=y.useState(t),o=l[0],i=l[1],u=e!==void 0,s=r.current;return r.current=u,!u&&s&&o!==t&&i(t),[u?e:o,y.useCallback(function(a){for(var m=arguments.length,h=new Array(m>1?m-1:0),d=1;d{o.target===e&&(l(),t(o))},n+r)}function Uh(e){e.offsetHeight}const aa=e=>!e||typeof e=="function"?e:t=>{e.current=t};function Bh(e,t){const n=aa(e),r=aa(t);return l=>{n&&n(l),r&&r(l)}}function mo(e,t){return y.useMemo(()=>Bh(e,t),[e,t])}function Hh(e){return e&&"setState"in e?Tn.findDOMNode(e):e??null}const Wh=Wt.forwardRef(({onEnter:e,onEntering:t,onEntered:n,onExit:r,onExiting:l,onExited:o,addEndListener:i,children:u,childRef:s,...a},m)=>{const h=y.useRef(null),d=mo(h,s),w=E=>{d(Hh(E))},g=E=>N=>{E&&h.current&&E(h.current,N)},k=y.useCallback(g(e),[e]),R=y.useCallback(g(t),[t]),p=y.useCallback(g(n),[n]),c=y.useCallback(g(r),[r]),v=y.useCallback(g(l),[l]),S=y.useCallback(g(o),[o]),T=y.useCallback(g(i),[i]);return f.jsx(zh,{ref:m,...a,onEnter:k,onEntered:p,onEntering:R,onExit:c,onExited:S,onExiting:v,addEndListener:T,nodeRef:h,children:typeof u=="function"?(E,N)=>u(E,{...N,ref:w}):Wt.cloneElement(u,{ref:w})})}),Vh=Wh;function Qh(e){const t=y.useRef(e);return y.useEffect(()=>{t.current=e},[e]),t}function Me(e){const t=Qh(e);return y.useCallback(function(...n){return t.current&&t.current(...n)},[t])}const Vf=e=>y.forwardRef((t,n)=>f.jsx("div",{...t,ref:n,className:M(t.className,e)})),Qf=Vf("h4");Qf.displayName="DivStyledAsH4";const Kf=y.forwardRef(({className:e,bsPrefix:t,as:n=Qf,...r},l)=>(t=H(t,"alert-heading"),f.jsx(n,{ref:l,className:M(e,t),...r})));Kf.displayName="AlertHeading";const Kh=Kf;function Gh(){return y.useState(null)}function Yh(){const e=y.useRef(!0),t=y.useRef(()=>e.current);return y.useEffect(()=>(e.current=!0,()=>{e.current=!1}),[]),t.current}function Xh(e){const t=y.useRef(null);return y.useEffect(()=>{t.current=e}),t.current}const Zh=typeof global<"u"&&global.navigator&&global.navigator.product==="ReactNative",Jh=typeof document<"u",ca=Jh||Zh?y.useLayoutEffect:y.useEffect,qh=["as","disabled"];function bh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function ev(e){return!e||e.trim()==="#"}function Hu({tagName:e,disabled:t,href:n,target:r,rel:l,role:o,onClick:i,tabIndex:u=0,type:s}){e||(n!=null||r!=null||l!=null?e="a":e="button");const a={tagName:e};if(e==="button")return[{type:s||"button",disabled:t},a];const m=d=>{if((t||e==="a"&&ev(n))&&d.preventDefault(),t){d.stopPropagation();return}i==null||i(d)},h=d=>{d.key===" "&&(d.preventDefault(),m(d))};return e==="a"&&(n||(n="#"),t&&(n=void 0)),[{role:o??"button",disabled:void 0,tabIndex:t?void 0:u,href:n,target:e==="a"?r:void 0,"aria-disabled":t||void 0,rel:e==="a"?l:void 0,onClick:m,onKeyDown:h},a]}const tv=y.forwardRef((e,t)=>{let{as:n,disabled:r}=e,l=bh(e,qh);const[o,{tagName:i}]=Hu(Object.assign({tagName:n,disabled:r},l));return f.jsx(i,Object.assign({},l,o,{ref:t}))});tv.displayName="Button";const nv=["onKeyDown"];function rv(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}function lv(e){return!e||e.trim()==="#"}const Gf=y.forwardRef((e,t)=>{let{onKeyDown:n}=e,r=rv(e,nv);const[l]=Hu(Object.assign({tagName:"a"},r)),o=Me(i=>{l.onKeyDown(i),n==null||n(i)});return lv(r.href)||r.role==="button"?f.jsx("a",Object.assign({ref:t},r,l,{onKeyDown:o})):f.jsx("a",Object.assign({ref:t},r,{onKeyDown:n}))});Gf.displayName="Anchor";const ov=Gf,Yf=y.forwardRef(({className:e,bsPrefix:t,as:n=ov,...r},l)=>(t=H(t,"alert-link"),f.jsx(n,{ref:l,className:M(e,t),...r})));Yf.displayName="AlertLink";const iv=Yf,uv={[St]:"show",[Ht]:"show"},Xf=y.forwardRef(({className:e,children:t,transitionClasses:n={},onEnter:r,...l},o)=>{const i={in:!1,timeout:300,mountOnEnter:!1,unmountOnExit:!1,appear:!1,...l},u=y.useCallback((s,a)=>{Uh(s),r==null||r(s,a)},[r]);return f.jsx(Vh,{ref:o,addEndListener:Ah,...i,onEnter:u,childRef:t.ref,children:(s,a)=>y.cloneElement(t,{...a,className:M("fade",e,t.props.className,uv[s],n[s])})})});Xf.displayName="Fade";const Ql=Xf,sv={"aria-label":it.string,onClick:it.func,variant:it.oneOf(["white"])},Wu=y.forwardRef(({className:e,variant:t,"aria-label":n="Close",...r},l)=>f.jsx("button",{ref:l,type:"button",className:M("btn-close",t&&`btn-close-${t}`,e),"aria-label":n,...r}));Wu.displayName="CloseButton";Wu.propTypes=sv;const Zf=Wu,Jf=y.forwardRef((e,t)=>{const{bsPrefix:n,show:r=!0,closeLabel:l="Close alert",closeVariant:o,className:i,children:u,variant:s="primary",onClose:a,dismissible:m,transition:h=Ql,...d}=yh(e,{show:"onClose"}),w=H(n,"alert"),g=Me(p=>{a&&a(!1,p)}),k=h===!0?Ql:h,R=f.jsxs("div",{role:"alert",...k?void 0:d,ref:t,className:M(i,w,s&&`${w}-${s}`,m&&`${w}-dismissible`),children:[m&&f.jsx(Zf,{onClick:g,"aria-label":l,variant:o}),u]});return k?f.jsx(k,{unmountOnExit:!0,...d,ref:void 0,in:r,children:R}):r?R:null});Jf.displayName="Alert";const fa=Object.assign(Jf,{Link:iv,Heading:Kh}),qf=y.forwardRef(({as:e,bsPrefix:t,variant:n="primary",size:r,active:l=!1,disabled:o=!1,className:i,...u},s)=>{const a=H(t,"btn"),[m,{tagName:h}]=Hu({tagName:e,disabled:o,...u}),d=h;return f.jsx(d,{...m,...u,ref:s,disabled:o,className:M(i,a,l&&"active",n&&`${a}-${n}`,r&&`${a}-${r}`,u.href&&o&&"disabled")})});qf.displayName="Button";const tn=qf;function av(e){const t=y.useRef(e);return t.current=e,t}function bf(e){const t=av(e);y.useEffect(()=>()=>t.current(),[])}function cv(e,t){let n=0;return y.Children.map(e,r=>y.isValidElement(r)?t(r,n++):r)}function fv(e,t){return y.Children.toArray(e).some(n=>y.isValidElement(n)&&n.type===t)}function dv({as:e,bsPrefix:t,className:n,...r}){t=H(t,"col");const l=$f(),o=If(),i=[],u=[];return l.forEach(s=>{const a=r[s];delete r[s];let m,h,d;typeof a=="object"&&a!=null?{span:m,offset:h,order:d}=a:m=a;const w=s!==o?`-${s}`:"";m&&i.push(m===!0?`${t}${w}`:`${t}${w}-${m}`),d!=null&&u.push(`order${w}-${d}`),h!=null&&u.push(`offset${w}-${h}`)}),[{...r,className:M(n,...i,...u)},{as:e,bsPrefix:t,spans:i}]}const ed=y.forwardRef((e,t)=>{const[{className:n,...r},{as:l="div",bsPrefix:o,spans:i}]=dv(e);return f.jsx(l,{...r,ref:t,className:M(n,!i.length&&o)})});ed.displayName="Col";const Vu=ed,td=y.forwardRef(({bsPrefix:e,fluid:t=!1,as:n="div",className:r,...l},o)=>{const i=H(e,"container"),u=typeof t=="string"?`-${t}`:"-fluid";return f.jsx(n,{ref:o,...l,className:M(r,t?`${i}${u}`:i)})});td.displayName="Container";const pv=td;var mv=Function.prototype.bind.call(Function.prototype.call,[].slice);function dn(e,t){return mv(e.querySelectorAll(t))}function da(e,t){if(e.contains)return e.contains(t);if(e.compareDocumentPosition)return e===t||!!(e.compareDocumentPosition(t)&16)}const hv="data-rr-ui-";function vv(e){return`${hv}${e}`}const nd=y.createContext(Vn?window:void 0);nd.Provider;function Qu(){return y.useContext(nd)}const yv={type:it.string,tooltip:it.bool,as:it.elementType},Ku=y.forwardRef(({as:e="div",className:t,type:n="valid",tooltip:r=!1,...l},o)=>f.jsx(e,{...l,ref:o,className:M(t,`${n}-${r?"tooltip":"feedback"}`)}));Ku.displayName="Feedback";Ku.propTypes=yv;const rd=Ku,gv=y.createContext({}),ft=gv,ld=y.forwardRef(({id:e,bsPrefix:t,className:n,type:r="checkbox",isValid:l=!1,isInvalid:o=!1,as:i="input",...u},s)=>{const{controlId:a}=y.useContext(ft);return t=H(t,"form-check-input"),f.jsx(i,{...u,ref:s,type:r,id:e||a,className:M(n,t,l&&"is-valid",o&&"is-invalid")})});ld.displayName="FormCheckInput";const od=ld,id=y.forwardRef(({bsPrefix:e,className:t,htmlFor:n,...r},l)=>{const{controlId:o}=y.useContext(ft);return e=H(e,"form-check-label"),f.jsx("label",{...r,ref:l,htmlFor:n||o,className:M(t,e)})});id.displayName="FormCheckLabel";const Gi=id,ud=y.forwardRef(({id:e,bsPrefix:t,bsSwitchPrefix:n,inline:r=!1,reverse:l=!1,disabled:o=!1,isValid:i=!1,isInvalid:u=!1,feedbackTooltip:s=!1,feedback:a,feedbackType:m,className:h,style:d,title:w="",type:g="checkbox",label:k,children:R,as:p="input",...c},v)=>{t=H(t,"form-check"),n=H(n,"form-switch");const{controlId:S}=y.useContext(ft),T=y.useMemo(()=>({controlId:e||S}),[S,e]),E=!R&&k!=null&&k!==!1||fv(R,Gi),N=f.jsx(od,{...c,type:g==="switch"?"checkbox":g,ref:v,isValid:i,isInvalid:u,disabled:o,as:p});return f.jsx(ft.Provider,{value:T,children:f.jsx("div",{style:d,className:M(h,E&&t,r&&`${t}-inline`,l&&`${t}-reverse`,g==="switch"&&n),children:R||f.jsxs(f.Fragment,{children:[N,E&&f.jsx(Gi,{title:w,children:k}),a&&f.jsx(rd,{type:m,tooltip:s,children:a})]})})})});ud.displayName="FormCheck";const Kl=Object.assign(ud,{Input:od,Label:Gi}),sd=y.forwardRef(({bsPrefix:e,type:t,size:n,htmlSize:r,id:l,className:o,isValid:i=!1,isInvalid:u=!1,plaintext:s,readOnly:a,as:m="input",...h},d)=>{const{controlId:w}=y.useContext(ft);return e=H(e,"form-control"),f.jsx(m,{...h,type:t,size:r,ref:d,readOnly:a,id:l||w,className:M(o,s?`${e}-plaintext`:e,n&&`${e}-${n}`,t==="color"&&`${e}-color`,i&&"is-valid",u&&"is-invalid")})});sd.displayName="FormControl";const wv=Object.assign(sd,{Feedback:rd}),ad=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"form-floating"),f.jsx(n,{ref:l,className:M(e,t),...r})));ad.displayName="FormFloating";const Sv=ad,cd=y.forwardRef(({controlId:e,as:t="div",...n},r)=>{const l=y.useMemo(()=>({controlId:e}),[e]);return f.jsx(ft.Provider,{value:l,children:f.jsx(t,{...n,ref:r})})});cd.displayName="FormGroup";const fd=cd,dd=y.forwardRef(({as:e="label",bsPrefix:t,column:n=!1,visuallyHidden:r=!1,className:l,htmlFor:o,...i},u)=>{const{controlId:s}=y.useContext(ft);t=H(t,"form-label");let a="col-form-label";typeof n=="string"&&(a=`${a} ${a}-${n}`);const m=M(l,t,r&&"visually-hidden",n&&a);return o=o||s,n?f.jsx(Vu,{ref:u,as:"label",className:m,htmlFor:o,...i}):f.jsx(e,{ref:u,className:m,htmlFor:o,...i})});dd.displayName="FormLabel";const xv=dd,pd=y.forwardRef(({bsPrefix:e,className:t,id:n,...r},l)=>{const{controlId:o}=y.useContext(ft);return e=H(e,"form-range"),f.jsx("input",{...r,type:"range",ref:l,className:M(t,e),id:n||o})});pd.displayName="FormRange";const kv=pd,md=y.forwardRef(({bsPrefix:e,size:t,htmlSize:n,className:r,isValid:l=!1,isInvalid:o=!1,id:i,...u},s)=>{const{controlId:a}=y.useContext(ft);return e=H(e,"form-select"),f.jsx("select",{...u,size:n,ref:s,className:M(r,e,t&&`${e}-${t}`,l&&"is-valid",o&&"is-invalid"),id:i||a})});md.displayName="FormSelect";const Ev=md,hd=y.forwardRef(({bsPrefix:e,className:t,as:n="small",muted:r,...l},o)=>(e=H(e,"form-text"),f.jsx(n,{...l,ref:o,className:M(t,e,r&&"text-muted")})));hd.displayName="FormText";const Cv=hd,vd=y.forwardRef((e,t)=>f.jsx(Kl,{...e,ref:t,type:"switch"}));vd.displayName="Switch";const Nv=Object.assign(vd,{Input:Kl.Input,Label:Kl.Label}),yd=y.forwardRef(({bsPrefix:e,className:t,children:n,controlId:r,label:l,...o},i)=>(e=H(e,"form-floating"),f.jsxs(fd,{ref:i,className:M(t,e),controlId:r,...o,children:[n,f.jsx("label",{htmlFor:r,children:l})]})));yd.displayName="FloatingLabel";const Tv=yd,_v={_ref:it.any,validated:it.bool,as:it.elementType},Gu=y.forwardRef(({className:e,validated:t,as:n="form",...r},l)=>f.jsx(n,{...r,ref:l,className:M(e,t&&"was-validated")}));Gu.displayName="Form";Gu.propTypes=_v;const pe=Object.assign(Gu,{Group:fd,Control:wv,Floating:Sv,Check:Kl,Switch:Nv,Label:xv,Text:Cv,Range:kv,Select:Ev,FloatingLabel:Tv});var ul;function pa(e){if((!ul&&ul!==0||e)&&Vn){var t=document.createElement("div");t.style.position="absolute",t.style.top="-9999px",t.style.width="50px",t.style.height="50px",t.style.overflow="scroll",document.body.appendChild(t),ul=t.offsetWidth-t.clientWidth,document.body.removeChild(t)}return ul}function Wo(e){e===void 0&&(e=po());try{var t=e.activeElement;return!t||!t.nodeName?null:t}catch{return e.body}}function jv(e=document){const t=e.defaultView;return Math.abs(t.innerWidth-e.documentElement.clientWidth)}const ma=vv("modal-open");class Rv{constructor({ownerDocument:t,handleContainerOverflow:n=!0,isRTL:r=!1}={}){this.handleContainerOverflow=n,this.isRTL=r,this.modals=[],this.ownerDocument=t}getScrollbarWidth(){return jv(this.ownerDocument)}getElement(){return(this.ownerDocument||document).body}setModalAttributes(t){}removeModalAttributes(t){}setContainerStyle(t){const n={overflow:"hidden"},r=this.isRTL?"paddingLeft":"paddingRight",l=this.getElement();t.style={overflow:l.style.overflow,[r]:l.style[r]},t.scrollBarWidth&&(n[r]=`${parseInt(Xt(l,r)||"0",10)+t.scrollBarWidth}px`),l.setAttribute(ma,""),Xt(l,n)}reset(){[...this.modals].forEach(t=>this.remove(t))}removeContainerStyle(t){const n=this.getElement();n.removeAttribute(ma),Object.assign(n.style,t.style)}add(t){let n=this.modals.indexOf(t);return n!==-1||(n=this.modals.length,this.modals.push(t),this.setModalAttributes(t),n!==0)||(this.state={scrollBarWidth:this.getScrollbarWidth(),style:{}},this.handleContainerOverflow&&this.setContainerStyle(this.state)),n}remove(t){const n=this.modals.indexOf(t);n!==-1&&(this.modals.splice(n,1),!this.modals.length&&this.handleContainerOverflow&&this.removeContainerStyle(this.state),this.removeModalAttributes(t))}isTopModal(t){return!!this.modals.length&&this.modals[this.modals.length-1]===t}}const Yu=Rv,Vo=(e,t)=>Vn?e==null?(t||po()).body:(typeof e=="function"&&(e=e()),e&&"current"in e&&(e=e.current),e&&("nodeType"in e||e.getBoundingClientRect)?e:null):null;function Lv(e,t){const n=Qu(),[r,l]=y.useState(()=>Vo(e,n==null?void 0:n.document));if(!r){const o=Vo(e);o&&l(o)}return y.useEffect(()=>{t&&r&&t(r)},[t,r]),y.useEffect(()=>{const o=Vo(e);o!==r&&l(o)},[e,r]),r}function Ov({children:e,in:t,onExited:n,mountOnEnter:r,unmountOnExit:l}){const o=y.useRef(null),i=y.useRef(t),u=Me(n);y.useEffect(()=>{t?i.current=!0:u(o.current)},[t,u]);const s=mo(o,e.ref),a=y.cloneElement(e,{ref:s});return t?a:l||!i.current&&r?null:a}function Pv({in:e,onTransition:t}){const n=y.useRef(null),r=y.useRef(!0),l=Me(t);return ca(()=>{if(!n.current)return;let o=!1;return l({in:e,element:n.current,initial:r.current,isStale:()=>o}),()=>{o=!0}},[e,l]),ca(()=>(r.current=!1,()=>{r.current=!0}),[]),n}function Fv({children:e,in:t,onExited:n,onEntered:r,transition:l}){const[o,i]=y.useState(!t);t&&o&&i(!1);const u=Pv({in:!!t,onTransition:a=>{const m=()=>{a.isStale()||(a.in?r==null||r(a.element,a.initial):(i(!0),n==null||n(a.element)))};Promise.resolve(l(a)).then(m,h=>{throw a.in||i(!0),h})}}),s=mo(u,e.ref);return o&&!t?null:y.cloneElement(e,{ref:s})}function ha(e,t,n){return e?f.jsx(e,Object.assign({},n)):t?f.jsx(Fv,Object.assign({},n,{transition:t})):f.jsx(Ov,Object.assign({},n))}function Mv(e){return e.code==="Escape"||e.keyCode===27}const zv=["show","role","className","style","children","backdrop","keyboard","onBackdropClick","onEscapeKeyDown","transition","runTransition","backdropTransition","runBackdropTransition","autoFocus","enforceFocus","restoreFocus","restoreFocusOptions","renderDialog","renderBackdrop","manager","container","onShow","onHide","onExit","onExited","onExiting","onEnter","onEntering","onEntered"];function $v(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,o;for(o=0;o=0)&&(n[l]=e[l]);return n}let Qo;function Iv(e){return Qo||(Qo=new Yu({ownerDocument:e==null?void 0:e.document})),Qo}function Dv(e){const t=Qu(),n=e||Iv(t),r=y.useRef({dialog:null,backdrop:null});return Object.assign(r.current,{add:()=>n.add(r.current),remove:()=>n.remove(r.current),isTopModal:()=>n.isTopModal(r.current),setDialogRef:y.useCallback(l=>{r.current.dialog=l},[]),setBackdropRef:y.useCallback(l=>{r.current.backdrop=l},[])})}const gd=y.forwardRef((e,t)=>{let{show:n=!1,role:r="dialog",className:l,style:o,children:i,backdrop:u=!0,keyboard:s=!0,onBackdropClick:a,onEscapeKeyDown:m,transition:h,runTransition:d,backdropTransition:w,runBackdropTransition:g,autoFocus:k=!0,enforceFocus:R=!0,restoreFocus:p=!0,restoreFocusOptions:c,renderDialog:v,renderBackdrop:S=K=>f.jsx("div",Object.assign({},K)),manager:T,container:E,onShow:N,onHide:j=()=>{},onExit:U,onExited:P,onExiting:ie,onEnter:Ke,onEntering:Ge,onEntered:on}=e,Qn=$v(e,zv);const Re=Qu(),Ye=Lv(E),C=Dv(T),L=Yh(),O=Xh(n),[I,A]=y.useState(!n),fe=y.useRef(null);y.useImperativeHandle(t,()=>C,[C]),Vn&&!O&&n&&(fe.current=Wo(Re==null?void 0:Re.document)),n&&I&&A(!1);const Le=Me(()=>{if(C.add(),sn.current=Vl(document,"keydown",ho),un.current=Vl(document,"focus",()=>setTimeout(Oe),!0),N&&N(),k){var K,Hr;const Yn=Wo((K=(Hr=C.dialog)==null?void 0:Hr.ownerDocument)!=null?K:Re==null?void 0:Re.document);C.dialog&&Yn&&!da(C.dialog,Yn)&&(fe.current=Yn,C.dialog.focus())}}),et=Me(()=>{if(C.remove(),sn.current==null||sn.current(),un.current==null||un.current(),p){var K;(K=fe.current)==null||K.focus==null||K.focus(c),fe.current=null}});y.useEffect(()=>{!n||!Ye||Le()},[n,Ye,Le]),y.useEffect(()=>{I&&et()},[I,et]),bf(()=>{et()});const Oe=Me(()=>{if(!R||!L()||!C.isTopModal())return;const K=Wo(Re==null?void 0:Re.document);C.dialog&&K&&!da(C.dialog,K)&&C.dialog.focus()}),mt=Me(K=>{K.target===K.currentTarget&&(a==null||a(K),u===!0&&j())}),ho=Me(K=>{s&&Mv(K)&&C.isTopModal()&&(m==null||m(K),K.defaultPrevented||j())}),un=y.useRef(),sn=y.useRef(),Kn=(...K)=>{A(!0),P==null||P(...K)};if(!Ye)return null;const Br=Object.assign({role:r,ref:C.setDialogRef,"aria-modal":r==="dialog"?!0:void 0},Qn,{style:o,className:l,tabIndex:-1});let Gn=v?v(Br):f.jsx("div",Object.assign({},Br,{children:y.cloneElement(i,{role:"document"})}));Gn=ha(h,d,{unmountOnExit:!0,mountOnEnter:!0,appear:!0,in:!!n,onExit:U,onExiting:ie,onExited:Kn,onEnter:Ke,onEntering:Ge,onEntered:on,children:Gn});let Dt=null;return u&&(Dt=S({ref:C.setBackdropRef,onClick:mt}),Dt=ha(w,g,{in:!!n,appear:!0,mountOnEnter:!0,unmountOnExit:!0,children:Dt})),f.jsx(f.Fragment,{children:Tn.createPortal(f.jsxs(f.Fragment,{children:[Dt,Gn]}),Ye)})});gd.displayName="Modal";const Av=Object.assign(gd,{Manager:Yu});function Uv(e,t){return e.classList?!!t&&e.classList.contains(t):(" "+(e.className.baseVal||e.className)+" ").indexOf(" "+t+" ")!==-1}function Bv(e,t){e.classList?e.classList.add(t):Uv(e,t)||(typeof e.className=="string"?e.className=e.className+" "+t:e.setAttribute("class",(e.className&&e.className.baseVal||"")+" "+t))}function va(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}function Hv(e,t){e.classList?e.classList.remove(t):typeof e.className=="string"?e.className=va(e.className,t):e.setAttribute("class",va(e.className&&e.className.baseVal||"",t))}const pn={FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"};class Wv extends Yu{adjustAndStore(t,n,r){const l=n.style[t];n.dataset[t]=l,Xt(n,{[t]:`${parseFloat(Xt(n,t))+r}px`})}restore(t,n){const r=n.dataset[t];r!==void 0&&(delete n.dataset[t],Xt(n,{[t]:r}))}setContainerStyle(t){super.setContainerStyle(t);const n=this.getElement();if(Bv(n,"modal-open"),!t.scrollBarWidth)return;const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";dn(n,pn.FIXED_CONTENT).forEach(o=>this.adjustAndStore(r,o,t.scrollBarWidth)),dn(n,pn.STICKY_CONTENT).forEach(o=>this.adjustAndStore(l,o,-t.scrollBarWidth)),dn(n,pn.NAVBAR_TOGGLER).forEach(o=>this.adjustAndStore(l,o,t.scrollBarWidth))}removeContainerStyle(t){super.removeContainerStyle(t);const n=this.getElement();Hv(n,"modal-open");const r=this.isRTL?"paddingLeft":"paddingRight",l=this.isRTL?"marginLeft":"marginRight";dn(n,pn.FIXED_CONTENT).forEach(o=>this.restore(r,o)),dn(n,pn.STICKY_CONTENT).forEach(o=>this.restore(l,o)),dn(n,pn.NAVBAR_TOGGLER).forEach(o=>this.restore(l,o))}}let Ko;function Vv(e){return Ko||(Ko=new Wv(e)),Ko}const wd=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-body"),f.jsx(n,{ref:l,className:M(e,t),...r})));wd.displayName="ModalBody";const Qv=wd,Kv=y.createContext({onHide(){}}),Sd=Kv,xd=y.forwardRef(({bsPrefix:e,className:t,contentClassName:n,centered:r,size:l,fullscreen:o,children:i,scrollable:u,...s},a)=>{e=H(e,"modal");const m=`${e}-dialog`,h=typeof o=="string"?`${e}-fullscreen-${o}`:`${e}-fullscreen`;return f.jsx("div",{...s,ref:a,className:M(m,t,l&&`${e}-${l}`,r&&`${m}-centered`,u&&`${m}-scrollable`,o&&h),children:f.jsx("div",{className:M(`${e}-content`,n),children:i})})});xd.displayName="ModalDialog";const kd=xd,Ed=y.forwardRef(({className:e,bsPrefix:t,as:n="div",...r},l)=>(t=H(t,"modal-footer"),f.jsx(n,{ref:l,className:M(e,t),...r})));Ed.displayName="ModalFooter";const Gv=Ed,Yv=y.forwardRef(({closeLabel:e="Close",closeVariant:t,closeButton:n=!1,onHide:r,children:l,...o},i)=>{const u=y.useContext(Sd),s=Me(()=>{u==null||u.onHide(),r==null||r()});return f.jsxs("div",{ref:i,...o,children:[l,n&&f.jsx(Zf,{"aria-label":e,variant:t,onClick:s})]})}),Xv=Yv,Cd=y.forwardRef(({bsPrefix:e,className:t,closeLabel:n="Close",closeButton:r=!1,...l},o)=>(e=H(e,"modal-header"),f.jsx(Xv,{ref:o,...l,className:M(t,e),closeLabel:n,closeButton:r})));Cd.displayName="ModalHeader";const Zv=Cd,Jv=Vf("h4"),Nd=y.forwardRef(({className:e,bsPrefix:t,as:n=Jv,...r},l)=>(t=H(t,"modal-title"),f.jsx(n,{ref:l,className:M(e,t),...r})));Nd.displayName="ModalTitle";const qv=Nd;function bv(e){return f.jsx(Ql,{...e,timeout:null})}function ey(e){return f.jsx(Ql,{...e,timeout:null})}const Td=y.forwardRef(({bsPrefix:e,className:t,style:n,dialogClassName:r,contentClassName:l,children:o,dialogAs:i=kd,"aria-labelledby":u,"aria-describedby":s,"aria-label":a,show:m=!1,animation:h=!0,backdrop:d=!0,keyboard:w=!0,onEscapeKeyDown:g,onShow:k,onHide:R,container:p,autoFocus:c=!0,enforceFocus:v=!0,restoreFocus:S=!0,restoreFocusOptions:T,onEntered:E,onExit:N,onExiting:j,onEnter:U,onEntering:P,onExited:ie,backdropClassName:Ke,manager:Ge,...on},Qn)=>{const[Re,Ye]=y.useState({}),[C,L]=y.useState(!1),O=y.useRef(!1),I=y.useRef(!1),A=y.useRef(null),[fe,Le]=Gh(),et=mo(Qn,Le),Oe=Me(R),mt=xh();e=H(e,"modal");const ho=y.useMemo(()=>({onHide:Oe}),[Oe]);function un(){return Ge||Vv({isRTL:mt})}function sn($){if(!Vn)return;const an=un().getScrollbarWidth()>0,Zu=$.scrollHeight>po($).documentElement.clientHeight;Ye({paddingRight:an&&!Zu?pa():void 0,paddingLeft:!an&&Zu?pa():void 0})}const Kn=Me(()=>{fe&&sn(fe.dialog)});bf(()=>{Ki(window,"resize",Kn),A.current==null||A.current()});const Br=()=>{O.current=!0},Gn=$=>{O.current&&fe&&$.target===fe.dialog&&(I.current=!0),O.current=!1},Dt=()=>{L(!0),A.current=Wf(fe.dialog,()=>{L(!1)})},K=$=>{$.target===$.currentTarget&&Dt()},Hr=$=>{if(d==="static"){K($);return}if(I.current||$.target!==$.currentTarget){I.current=!1;return}R==null||R()},Yn=$=>{w?g==null||g($):($.preventDefault(),d==="static"&&Dt())},Id=($,an)=>{$&&sn($),U==null||U($,an)},Dd=$=>{A.current==null||A.current(),N==null||N($)},Ad=($,an)=>{P==null||P($,an),Hf(window,"resize",Kn)},Ud=$=>{$&&($.style.display=""),ie==null||ie($),Ki(window,"resize",Kn)},Bd=y.useCallback($=>f.jsx("div",{...$,className:M(`${e}-backdrop`,Ke,!h&&"show")}),[h,Ke,e]),Xu={...n,...Re};Xu.display="block";const Hd=$=>f.jsx("div",{role:"dialog",...$,style:Xu,className:M(t,e,C&&`${e}-static`,!h&&"show"),onClick:d?Hr:void 0,onMouseUp:Gn,"aria-label":a,"aria-labelledby":u,"aria-describedby":s,children:f.jsx(i,{...on,onMouseDown:Br,className:r,contentClassName:l,children:o})});return f.jsx(Sd.Provider,{value:ho,children:f.jsx(Av,{show:m,ref:et,backdrop:d,container:p,keyboard:!0,autoFocus:c,enforceFocus:v,restoreFocus:S,restoreFocusOptions:T,onEscapeKeyDown:Yn,onShow:k,onHide:R,onEnter:Id,onEntering:Ad,onEntered:E,onExit:Dd,onExiting:j,onExited:Ud,manager:un(),transition:h?bv:void 0,backdropTransition:h?ey:void 0,renderBackdrop:Bd,renderDialog:Hd})})});Td.displayName="Modal";const ge=Object.assign(Td,{Body:Qv,Header:Zv,Title:qv,Footer:Gv,Dialog:kd,TRANSITION_DURATION:300,BACKDROP_TRANSITION_DURATION:150}),ya=1e3;function ty(e,t,n){const r=(e-t)/(n-t)*100;return Math.round(r*ya)/ya}function ga({min:e,now:t,max:n,label:r,visuallyHidden:l,striped:o,animated:i,className:u,style:s,variant:a,bsPrefix:m,...h},d){return f.jsx("div",{ref:d,...h,role:"progressbar",className:M(u,`${m}-bar`,{[`bg-${a}`]:a,[`${m}-bar-animated`]:i,[`${m}-bar-striped`]:i||o}),style:{width:`${ty(t,e,n)}%`,...s},"aria-valuenow":t,"aria-valuemin":e,"aria-valuemax":n,children:l?f.jsx("span",{className:"visually-hidden",children:r}):r})}const _d=y.forwardRef(({isChild:e=!1,...t},n)=>{const r={min:0,max:100,animated:!1,visuallyHidden:!1,striped:!1,...t};if(r.bsPrefix=H(r.bsPrefix,"progress"),e)return ga(r,n);const{min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:m,bsPrefix:h,variant:d,className:w,children:g,...k}=r;return f.jsx("div",{ref:n,...k,className:M(w,h),children:g?cv(g,R=>y.cloneElement(R,{isChild:!0})):ga({min:l,now:o,max:i,label:u,visuallyHidden:s,striped:a,animated:m,bsPrefix:h,variant:d},n)})});_d.displayName="ProgressBar";const ny=_d,jd=y.forwardRef(({bsPrefix:e,className:t,as:n="div",...r},l)=>{const o=H(e,"row"),i=$f(),u=If(),s=`${o}-cols`,a=[];return i.forEach(m=>{const h=r[m];delete r[m];let d;h!=null&&typeof h=="object"?{cols:d}=h:d=h;const w=m!==u?`-${m}`:"";d!=null&&a.push(`${s}${w}-${d}`)}),f.jsx(n,{ref:l,...r,className:M(t,o,...a)})});jd.displayName="Row";const Rd=jd,Ld=y.forwardRef(({bsPrefix:e,variant:t,animation:n="border",size:r,as:l="div",className:o,...i},u)=>{e=H(e,"spinner");const s=`${e}-${n}`;return f.jsx(l,{ref:u,...i,className:M(o,s,r&&`${s}-${r}`,t&&`text-${t}`)})});Ld.displayName="Spinner";const Un=Ld,Gl="initializing",Od="paused",Pd="live",ry="error",ln=y.createContext({listTorrents:()=>{throw new Error("Function not implemented.")},getTorrentDetails:()=>{throw new Error("Function not implemented.")},getTorrentStats:()=>{throw new Error("Function not implemented.")},uploadTorrent:()=>{throw new Error("Function not implemented.")},pause:()=>{throw new Error("Function not implemented.")},start:()=>{throw new Error("Function not implemented.")},forget:()=>{throw new Error("Function not implemented.")},delete:()=>{throw new Error("Function not implemented.")}}),Ur=y.createContext({setCloseableError:e=>{},refreshTorrents:()=>{}}),Fd=y.createContext({refresh:()=>{}}),Go=({className:e,onClick:t,disabled:n,color:r})=>{const l=o=>{o.stopPropagation(),!n&&t()};return f.jsx("a",{className:`bi ${e} p-1`,onClick:l,href:"#"})},ly=({id:e,show:t,onHide:n})=>{if(!t)return null;const[r,l]=y.useState(!1),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(Ur),m=y.useContext(ln),h=()=>{l(!1),i(null),s(!1),n()},d=()=>{s(!0),(r?m.delete:m.forget)(e).then(()=>{a.refreshTorrents(),h()}).catch(g=>{i({text:`Error deleting torrent id=${e}`,details:g}),s(!1)})};return f.jsxs(ge,{show:t,onHide:h,children:[f.jsx(ge.Header,{closeButton:!0,children:"Delete torrent"}),f.jsxs(ge.Body,{children:[f.jsx(pe,{children:f.jsx(pe.Group,{controlId:"delete-torrent",children:f.jsx(pe.Check,{type:"checkbox",label:"Also delete files",checked:r,onChange:()=>l(!r)})})}),o&&f.jsx(Mr,{error:o})]}),f.jsxs(ge.Footer,{children:[u&&f.jsx(Un,{}),f.jsx(tn,{variant:"primary",onClick:d,disabled:u,children:"OK"}),f.jsx(tn,{variant:"secondary",onClick:h,children:"Cancel"})]})]})},oy=({id:e,statsResponse:t})=>{let n=t.state,[r,l]=y.useState(!1),[o,i]=y.useState(!1),u=y.useContext(Fd);const s=n=="live",a=n=="paused"||n=="error",m=y.useContext(Ur),h=y.useContext(ln),d=()=>{l(!0),h.start(e).then(()=>{u.refresh()},R=>{m.setCloseableError({text:`Error starting torrent id=${e}`,details:R})}).finally(()=>l(!1))},w=()=>{l(!0),h.pause(e).then(()=>{u.refresh()},R=>{m.setCloseableError({text:`Error pausing torrent id=${e}`,details:R})}).finally(()=>l(!1))},g=()=>{l(!0),i(!0)},k=()=>{l(!1),i(!1)};return f.jsx(Rd,{children:f.jsxs(Vu,{children:[a&&f.jsx(Go,{className:"bi-play-circle",onClick:d,disabled:r,color:"success"}),s&&f.jsx(Go,{className:"bi-pause-circle",onClick:w,disabled:r}),f.jsx(Go,{className:"bi-x-circle",onClick:g,disabled:r,color:"danger"}),f.jsx(ly,{id:e,show:o,onHide:k})]})})},iy=({statsResponse:e})=>{switch(e.state){case Od:return"Paused";case Gl:return"Checking files";case ry:return"Error"}return e.state!="live"||e.live===null?e.state:f.jsxs(f.Fragment,{children:[!e.finished&&f.jsxs("p",{children:["↓ ",e.live.download_speed.human_readable]}),f.jsxs("p",{children:["↑ ",e.live.upload_speed.human_readable]})]})},uy=({id:e,detailsResponse:t,statsResponse:n})=>{const r=(n==null?void 0:n.state)??"",l=n==null?void 0:n.error,o=(n==null?void 0:n.total_bytes)??1,i=(n==null?void 0:n.progress_bytes)??0,u=(n==null?void 0:n.finished)||!1,s=l?100:i/o*100,a=(r==Gl||r==Pd)&&!u,m=l?"Error":`${s.toFixed(2)}%`,h=l?"danger":u?"success":r==Gl?"warning":"primary",d=()=>{var k;let g=(k=n==null?void 0:n.live)==null?void 0:k.snapshot.peer_stats;return g?`${g.live} / ${g.seen}`:""};let w=[];return l?w.push("bg-warning"):e%2==0&&w.push("bg-light"),f.jsxs(Rd,{className:w.join(" "),children:[f.jsx(vt,{size:3,label:"Name",children:t?f.jsxs(f.Fragment,{children:[f.jsx("div",{className:"text-truncate",children:gy(t)}),l&&f.jsxs("p",{className:"text-danger",children:[f.jsx("strong",{children:"Error:"})," ",l]})]}):f.jsx(Un,{})}),n?f.jsxs(f.Fragment,{children:[f.jsx(vt,{label:"Size",children:`${zd(o)} `}),f.jsx(vt,{size:2,label:(r==Od,"Progress"),children:f.jsx(ny,{now:s,label:m,animated:a,variant:h})}),f.jsx(vt,{size:2,label:"Speed",children:f.jsx(iy,{statsResponse:n})}),f.jsx(vt,{label:"ETA",children:wy(n)}),f.jsx(vt,{size:2,label:"Peers",children:d()}),f.jsx(vt,{label:"Actions",children:f.jsx(oy,{id:e,statsResponse:n})})]}):f.jsx(vt,{label:"Loading stats",size:8,children:f.jsx(Un,{})})]})},vt=({size:e,label:t,children:n})=>f.jsxs(Vu,{md:e||1,className:"py-3",children:[f.jsx("div",{className:"fw-bold",children:t}),n]}),sy=({id:e,torrent:t})=>{const[n,r]=y.useState(null),[l,o]=y.useState(null),[i,u]=y.useState(0),s=y.useContext(ln),a=()=>{u(i+1)};return y.useEffect(()=>{if(n===null)return xy(async()=>{await s.getTorrentDetails(t.id).then(r)},1e3)},[n]),y.useEffect(()=>$d(async()=>s.getTorrentStats(t.id).then(g=>(o(g),g)).then(g=>g.finished?1e4:g.state==Gl||g.state==Pd?1e3:1e4,()=>1e4),0),[i]),f.jsx(Fd.Provider,{value:{refresh:a},children:f.jsx(uy,{id:e,detailsResponse:n,statsResponse:l})})},ay=e=>{if(e.torrents===null&&e.loading)return f.jsx(Un,{});if(e.torrents!==null)return e.torrents.length===0?f.jsx("div",{className:"text-center",children:f.jsx("p",{children:"No existing torrents found. Add them through buttons below."})}):f.jsx("div",{style:{fontSize:"smaller"},children:e.torrents.map(t=>f.jsx(sy,{id:t.id,torrent:t},t.id))})},cy=e=>{const[t,n]=y.useState(null),[r,l]=y.useState(null),[o,i]=y.useState(null),[u,s]=y.useState(!1),a=y.useContext(ln),m=async()=>{s(!0);let d=await a.listTorrents().finally(()=>s(!1));i(d.torrents)};y.useEffect(()=>$d(async()=>m().then(()=>(l(null),5e3),d=>(l({text:"Error refreshing torrents",details:d}),console.error(d),5e3)),0),[]);const h={setCloseableError:n,refreshTorrents:m};return f.jsx(Ur.Provider,{value:h,children:f.jsxs("div",{className:"text-center",children:[f.jsx("h1",{className:"mt-3 mb-4",children:e.title}),f.jsx(yy,{closeableError:t,otherError:r,torrents:o,torrentsLoading:u})]})})},fy=e=>{let{details:t}=e;return t?f.jsxs(f.Fragment,{children:[t.statusText&&f.jsx("p",{children:f.jsx("strong",{children:t.statusText})}),f.jsx("pre",{children:t.text})]}):null},Mr=e=>{let{error:t,remove:n}=e;return t==null?null:f.jsxs(fa,{variant:"danger",onClose:n,dismissible:n!=null,children:[f.jsx(fa.Heading,{children:t.text}),f.jsx(fy,{details:t.details})]})},Md=({buttonText:e,onClick:t,data:n,resetData:r,variant:l})=>{const[o,i]=y.useState(!1),[u,s]=y.useState(null),[a,m]=y.useState(null),h=y.useContext(ln);y.useEffect(()=>{if(n===null)return;let w=setTimeout(async()=>{i(!0);try{const g=await h.uploadTorrent(n,{list_only:!0});s(g)}catch(g){m({text:"Error listing torrent files",details:g})}finally{i(!1)}},0);return()=>clearTimeout(w)},[n]);const d=()=>{r(),m(null),s(null),i(!1)};return f.jsxs(f.Fragment,{children:[f.jsx(tn,{variant:l,onClick:t,className:"m-1",children:e}),n&&f.jsx(hy,{onHide:d,listTorrentError:a,listTorrentResponse:u,data:n,listTorrentLoading:o})]})},dy=({show:e,setUrl:t,cancel:n})=>{let[r,l]=y.useState("");return f.jsxs(ge,{show:e,onHide:n,size:"lg",children:[f.jsx(ge.Header,{closeButton:!0,children:f.jsx(ge.Title,{children:"Add torrent"})}),f.jsx(ge.Body,{children:f.jsx(pe,{children:f.jsxs(pe.Group,{className:"mb-3",controlId:"url",children:[f.jsx(pe.Label,{children:"Enter magnet or HTTP(S) URL to the .torrent"}),f.jsx(pe.Control,{value:r,placeholder:"magnet:?xt=urn:btih:...",onChange:o=>{l(o.target.value)}})]})})}),f.jsxs(ge.Footer,{children:[f.jsx(tn,{variant:"primary",onClick:()=>{t(r),l("")},disabled:r.length==0,children:"OK"}),f.jsx(tn,{variant:"secondary",onClick:n,children:"Cancel"})]})]})},py=()=>{let[e,t]=y.useState(null),[n,r]=y.useState(!1);return f.jsxs(f.Fragment,{children:[f.jsx(Md,{variant:"primary",buttonText:"Add Torrent from Magnet / URL",onClick:()=>{r(!0)},data:e,resetData:()=>t(null)}),f.jsx(dy,{show:n,setUrl:l=>{r(!1),t(l)},cancel:()=>{r(!1),t(null)}})]})},my=()=>{const e=y.useRef(),[t,n]=y.useState(null),r=async()=>{var u;if(!((u=e==null?void 0:e.current)!=null&&u.files))return;const i=e.current.files[0];n(i)},l=()=>{e!=null&&e.current&&(e.current.value="",n(null))},o=()=>{e!=null&&e.current&&e.current.click()};return f.jsxs(f.Fragment,{children:[f.jsx("input",{type:"file",ref:e,accept:".torrent",onChange:r,className:"d-none"}),f.jsx(Md,{variant:"secondary",buttonText:"Upload .torrent File",onClick:o,data:t,resetData:l})]})},hy=e=>{let{onHide:t,listTorrentResponse:n,listTorrentError:r,listTorrentLoading:l,data:o}=e;const[i,u]=y.useState([]),[s,a]=y.useState(!1),[m,h]=y.useState(null),[d,w]=y.useState(!1),[g,k]=y.useState(""),R=y.useContext(Ur),p=y.useContext(ln);y.useEffect(()=>{console.log(n),u(n?n.details.files.map((E,N)=>N):[]),k((n==null?void 0:n.output_folder)||"")},[n]);const c=()=>{t(),u([]),h(null),a(!1)},v=E=>{i.includes(E)?u(i.filter(N=>N!==E)):u([...i,E])},S=async()=>{if(!n)return;a(!0);let E=n.seen_peers?n.seen_peers.slice(0,32):null,N={overwrite:!0,only_files:i,initial_peers:E,output_folder:g};d&&(N.peer_opts={connect_timeout:20,read_write_timeout:60}),p.uploadTorrent(o,N).then(()=>{t(),R.refreshTorrents()},j=>{h({text:"Error starting torrent",details:j})}).finally(()=>a(!1))},T=()=>{if(l)return f.jsx(Un,{});if(r)return f.jsx(Mr,{error:r});if(n)return f.jsxs(pe,{children:[f.jsxs("fieldset",{className:"mb-4",children:[f.jsx("legend",{children:"Pick the files to download"}),n.details.files.map((E,N)=>f.jsx(pe.Group,{controlId:`check-${N}`,children:f.jsx(pe.Check,{type:"checkbox",label:`${E.name} (${zd(E.length)})`,checked:i.includes(N),onChange:()=>v(N)})},N))]}),f.jsxs("fieldset",{children:[f.jsx("legend",{children:"Options"}),f.jsxs(pe.Group,{controlId:"output-folder",className:"mb-3",children:[f.jsx(pe.Label,{children:"Output folder"}),f.jsx(pe.Control,{type:"text",value:g,onChange:E=>k(E.target.value)})]}),f.jsxs(pe.Group,{controlId:"unpopular-torrent",className:"mb-3",children:[f.jsx(pe.Check,{type:"checkbox",label:"Increase timeouts",checked:d,onChange:()=>w(!d)}),f.jsx("small",{id:"emailHelp",className:"form-text text-muted",children:"This might be useful for unpopular torrents with few peers. It will slow down fast torrents though."})]})]})]})};return f.jsxs(ge,{show:!0,onHide:c,size:"lg",children:[f.jsx(ge.Header,{closeButton:!0,children:f.jsx(ge.Title,{children:"Add torrent"})}),f.jsxs(ge.Body,{children:[T(),f.jsx(Mr,{error:m})]}),f.jsxs(ge.Footer,{children:[s&&f.jsx(Un,{}),f.jsx(tn,{variant:"primary",onClick:S,disabled:l||s||i.length==0,children:"OK"}),f.jsx(tn,{variant:"secondary",onClick:c,children:"Cancel"})]})]})},vy=()=>f.jsxs("div",{id:"buttons-container",className:"mt-3",children:[f.jsx(py,{}),f.jsx(my,{})]}),yy=e=>{let t=y.useContext(Ur);return f.jsxs(pv,{children:[f.jsx(Mr,{error:e.closeableError,remove:()=>t.setCloseableError(null)}),f.jsx(Mr,{error:e.otherError}),f.jsx(ay,{torrents:e.torrents,loading:e.torrentsLoading}),f.jsx(vy,{})]})};function zd(e){if(e===0)return"0 Bytes";const t=1024,n=["Bytes","KB","MB","GB"],r=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,r)).toFixed(2))+" "+n[r]}function gy(e){return e.files.filter(n=>n.included).reduce((n,r)=>n.length>r.length?n:r).name}function wy(e){var n,r,l;let t=(l=(r=(n=e==null?void 0:e.live)==null?void 0:n.time_remaining)==null?void 0:r.duration)==null?void 0:l.secs;return t==null?"N/A":Sy(t)}function Sy(e){const t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=e%60,l=(o,i)=>o>0?`${o}${i}`:"";return t>0?`${l(t,"h")} ${l(n,"m")}`.trim():n>0?`${l(n,"m")} ${l(r,"s")}`.trim():`${l(r,"s")}`.trim()}function $d(e,t){let n,r=t;const l=async()=>{if(r=await e(),r==null)throw"asyncCallback returned null or undefined";o()};let o=()=>{n=setTimeout(l,r)};return o(),()=>{clearTimeout(n)}}function xy(e,t){let n;const r=async()=>{await e().then(()=>!1,()=>!0)&&l()};let l=o=>{n=setTimeout(r,o!==void 0?o:t)};return l(0),()=>clearTimeout(n)}const ky=window.origin==="null"||window.origin==="http://localhost:3031"?"http://localhost:3030":"",yt=async(e,t,n)=>{console.log(e,t);const r=ky+t,l={method:e,headers:{Accept:"application/json"},body:n};let o={method:e,path:t,text:""},i;try{i=await fetch(r,l)}catch{return o.text="network error",Promise.reject(o)}if(o.status=i.status,o.statusText=`${i.status} ${i.statusText}`,!i.ok){const s=await i.text();try{const a=JSON.parse(s);o.text=a.human_readable!==void 0?a.human_readable:JSON.stringify(a,null,2)}catch{o.text=s}return Promise.reject(o)}return await i.json()},Ey={listTorrents:()=>yt("GET","/torrents"),getTorrentDetails:e=>yt("GET",`/torrents/${e}`),getTorrentStats:e=>yt("GET",`/torrents/${e}/stats/v1`),uploadTorrent:(e,t)=>{var r,l;let n="/torrents?&overwrite=true";return t!=null&&t.list_only&&(n+="&list_only=true"),(t==null?void 0:t.only_files)!=null&&(n+=`&only_files=${t.only_files.join(",")}`),(r=t==null?void 0:t.peer_opts)!=null&&r.connect_timeout&&(n+=`&peer_connect_timeout=${t.peer_opts.connect_timeout}`),(l=t==null?void 0:t.peer_opts)!=null&&l.read_write_timeout&&(n+=`&peer_read_write_timeout=${t.peer_opts.read_write_timeout}`),t!=null&&t.initial_peers&&(n+=`&initial_peers=${t.initial_peers.join(",")}`),t!=null&&t.output_folder&&(n+=`&output_folder=${t.output_folder}`),typeof e=="string"&&(n+="&is_url=true"),yt("POST",n,e)},pause:e=>yt("POST",`/torrents/${e}/pause`),start:e=>yt("POST",`/torrents/${e}/start`),forget:e=>yt("POST",`/torrents/${e}/forget`),delete:e=>yt("POST",`/torrents/${e}/delete`)};Yo.createRoot(document.getElementById("app")).render(f.jsx(y.StrictMode,{children:f.jsx(ln.Provider,{value:Ey,children:f.jsx(cy,{title:"rqbit web UI - version 4.0.0"})})})); diff --git a/crates/librqbit/webui/dist/manifest.json b/crates/librqbit/webui/dist/manifest.json index f2846c2..9d5ab9c 100644 --- a/crates/librqbit/webui/dist/manifest.json +++ b/crates/librqbit/webui/dist/manifest.json @@ -4,7 +4,7 @@ "src": "assets/logo.svg" }, "index.html": { - "file": "assets/index-6d4556f3.js", + "file": "assets/index-713a95fc.js", "isEntry": true, "src": "index.html" } diff --git a/crates/librqbit/webui/src/api-types.ts b/crates/librqbit/webui/src/api-types.ts index afa103c..1a5fd44 100644 --- a/crates/librqbit/webui/src/api-types.ts +++ b/crates/librqbit/webui/src/api-types.ts @@ -27,6 +27,11 @@ export interface ListTorrentsResponse { torrents: Array; } +export interface Speed { + mbps: number; + human_readable: string; +} + // Interface for the Torrent Stats API response export interface LiveTorrentStats { snapshot: { @@ -52,10 +57,8 @@ export interface LiveTorrentStats { secs: number; nanos: number; }; - download_speed: { - mbps: number; - human_readable: string; - }; + download_speed: Speed; + upload_speed: Speed; all_time_download_speed: { mbps: number; human_readable: string; diff --git a/crates/librqbit/webui/src/rqbit-web.tsx b/crates/librqbit/webui/src/rqbit-web.tsx index 4869ac0..f9f7320 100644 --- a/crates/librqbit/webui/src/rqbit-web.tsx +++ b/crates/librqbit/webui/src/rqbit-web.tsx @@ -185,6 +185,27 @@ const TorrentActions: React.FC<{ } +const Speed: React.FC<{ statsResponse: TorrentStats }> = ({ statsResponse }) => { + switch (statsResponse.state) { + case STATE_PAUSED: return 'Paused'; + case STATE_INITIALIZING: return 'Checking files'; + case STATE_ERROR: return 'Error'; + } + // Unknown state + if (statsResponse.state != 'live' || statsResponse.live === null) { + return statsResponse.state; + } + + return <> + {!statsResponse.finished &&

↓ {statsResponse.live.download_speed.human_readable}

} +

↑ {statsResponse.live.upload_speed.human_readable}

+ + + if (statsResponse.finished) { + return Completed; + } +} + const TorrentRow: React.FC<{ id: number, detailsResponse: TorrentDetails | null, @@ -208,19 +229,6 @@ const TorrentRow: React.FC<{ return `${peer_stats.live} / ${peer_stats.seen}`; } - const formatDownloadSpeed = () => { - if (finished) { - return 'Completed'; - } - switch (state) { - case STATE_PAUSED: return 'Paused'; - case STATE_INITIALIZING: return 'Checking files'; - case STATE_ERROR: return 'Error'; - } - - return statsResponse?.live?.download_speed.human_readable ?? "N/A"; - } - let classNames = []; if (error) { @@ -253,7 +261,9 @@ const TorrentRow: React.FC<{ animated={isAnimated} variant={progressBarVariant} /> - {formatDownloadSpeed()} + + + {getCompletionETA(statsResponse)} {formatPeersString()} diff --git a/crates/librqbit_core/src/speed_estimator.rs b/crates/librqbit_core/src/speed_estimator.rs index bfd5ed9..6fbb1c7 100644 --- a/crates/librqbit_core/src/speed_estimator.rs +++ b/crates/librqbit_core/src/speed_estimator.rs @@ -8,14 +8,14 @@ use parking_lot::Mutex; #[derive(Clone, Copy)] struct ProgressSnapshot { - downloaded_bytes: u64, + progress_bytes: u64, instant: Instant, } -/// Estimates download speed in a sliding time window. +/// Estimates download/upload speed in a sliding time window. pub struct SpeedEstimator { latest_per_second_snapshots: Mutex>, - download_bytes_per_second: AtomicU64, + bytes_per_second: AtomicU64, time_remaining_millis: AtomicU64, } @@ -24,7 +24,7 @@ impl SpeedEstimator { assert!(window_seconds > 1); Self { latest_per_second_snapshots: Mutex::new(VecDeque::with_capacity(window_seconds)), - download_bytes_per_second: Default::default(), + bytes_per_second: Default::default(), time_remaining_millis: Default::default(), } } @@ -37,20 +37,25 @@ impl SpeedEstimator { Some(Duration::from_millis(tr)) } - pub fn download_bps(&self) -> u64 { - self.download_bytes_per_second.load(Ordering::Relaxed) + pub fn bps(&self) -> u64 { + self.bytes_per_second.load(Ordering::Relaxed) } - pub fn download_mbps(&self) -> f64 { - self.download_bps() as f64 / 1024f64 / 1024f64 + pub fn mbps(&self) -> f64 { + self.bps() as f64 / 1024f64 / 1024f64 } - pub fn add_snapshot(&self, downloaded_bytes: u64, remaining_bytes: u64, instant: Instant) { + pub fn add_snapshot( + &self, + progress_bytes: u64, + remaining_bytes: Option, + instant: Instant, + ) { let first = { let mut g = self.latest_per_second_snapshots.lock(); let current = ProgressSnapshot { - downloaded_bytes, + progress_bytes, instant, }; @@ -67,19 +72,18 @@ impl SpeedEstimator { } }; - let downloaded_bytes_diff = downloaded_bytes - first.downloaded_bytes; + let downloaded_bytes_diff = progress_bytes - first.progress_bytes; let elapsed = instant - first.instant; let bps = downloaded_bytes_diff as f64 / elapsed.as_secs_f64(); let time_remaining_millis_rounded: u64 = if downloaded_bytes_diff > 0 { - let time_remaining_secs = remaining_bytes as f64 / bps; + let time_remaining_secs = remaining_bytes.unwrap_or_default() as f64 / bps; (time_remaining_secs * 1000f64) as u64 } else { 0 }; self.time_remaining_millis .store(time_remaining_millis_rounded, Ordering::Relaxed); - self.download_bytes_per_second - .store(bps as u64, Ordering::Relaxed); + self.bytes_per_second.store(bps as u64, Ordering::Relaxed); } } diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index 502a180..72168af 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -377,7 +377,7 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> { None => continue }; let stats = handle.stats_snapshot(); - let speed = handle.speed_estimator(); + let speed = handle.down_speed_estimator(); let total = stats.total_bytes; let progress = stats.total_bytes - stats.remaining_bytes; let downloaded_pct = if stats.remaining_bytes == 0 { @@ -390,7 +390,7 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> { idx, downloaded_pct, SF::new(progress), - speed.download_mbps(), + speed.mbps(), SF::new(stats.fetched_bytes), SF::new(stats.remaining_bytes), SF::new(total), From 162afe30560f291a9605163ae1aa521477025d4e Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 21:13:31 +0000 Subject: [PATCH 10/28] DHT announce compiles --- crates/dht/src/dht.rs | 96 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 12 deletions(-) diff --git a/crates/dht/src/dht.rs b/crates/dht/src/dht.rs index 3581bfe..8a8863d 100644 --- a/crates/dht/src/dht.rs +++ b/crates/dht/src/dht.rs @@ -1,6 +1,7 @@ use std::{ cmp::Reverse, - net::SocketAddr, + net::{SocketAddr, SocketAddrV4}, + str::FromStr, sync::{ atomic::{AtomicU16, Ordering}, Arc, @@ -11,8 +12,8 @@ use std::{ use crate::{ bprotocol::{ - self, CompactNodeInfo, ErrorDescription, FindNodeRequest, GetPeersRequest, Message, - MessageKind, Node, PingRequest, Response, + self, AnnouncePeer, CompactNodeInfo, ErrorDescription, FindNodeRequest, GetPeersRequest, + Message, MessageKind, Node, PingRequest, Response, }, peer_store::PeerStore, routing_table::{InsertResult, NodeStatus, RoutingTable}, @@ -93,17 +94,52 @@ trait RecursiveRequestCallbacks: Sized + Send + Sync + 'static { ); } -struct RecursiveRequestCallbacksGetPeers {} +struct RecursiveRequestCallbacksGetPeers { + // Id20::from_str("00000fffffffffffffffffffffffffffffffffff").unwrap() + min_distance_to_announce: Id20, +} + impl RecursiveRequestCallbacks for RecursiveRequestCallbacksGetPeers { fn on_request_start(&self, _: &RecursiveRequest, _: Id20, _: SocketAddr) {} fn on_request_end( &self, - _: &RecursiveRequest, - _: Id20, - _: SocketAddr, - _: &anyhow::Result, + req: &RecursiveRequest, + target_node: Id20, + addr: SocketAddr, + resp: &anyhow::Result, ) { + let announce_addr = match req.dht.announce_addr { + Some(a) => a, + None => return, + }; + let resp = match resp { + Ok(ResponseOrError::Response(resp)) => resp, + _ => return, + }; + let token = match &resp.token { + Some(token) => token, + None => return, + }; + if req.info_hash.distance(&target_node) > self.min_distance_to_announce { + trace!( + "not announcing, {:?} is too far from {:?}", + target_node, + req.info_hash + ); + return; + } + let (tid, message) = req.dht.create_request(Request::Announce { + info_hash: req.info_hash, + token: token.clone(), + addr: announce_addr, + }); + + let _ = req.dht.worker_sender.send(WorkerSendRequest { + our_tid: Some(tid), + message, + addr, + }); } } @@ -165,7 +201,12 @@ impl RequestPeersStream { useful_nodes: RwLock::new(Vec::new()), peer_tx, node_tx, - callbacks: RecursiveRequestCallbacksGetPeers {}, + callbacks: RecursiveRequestCallbacksGetPeers { + min_distance_to_announce: Id20::from_str( + "000000ffffffffffffffffffffffffffffffffff", + ) + .unwrap(), + }, }); let join_handle = rp.request_peers_forever(node_rx); Self { @@ -351,7 +392,7 @@ impl RecursiveRequest { self.callbacks.on_request_start(self, id, addr); } - let response = self.dht.request(self.request, addr).await.map(|r| { + let response = self.dht.request(self.request.clone(), addr).await.map(|r| { self.mark_node_responded(addr, &r); r }); @@ -359,7 +400,7 @@ impl RecursiveRequest { self.callbacks.on_request_end(self, id, addr, &response); } - let response = match self.dht.request(self.request, addr).await { + let response = match self.dht.request(self.request.clone(), addr).await { Ok(ResponseOrError::Response(r)) => r, Ok(ResponseOrError::Error(e)) => bail!("error response: {:?}", e), Err(e) => { @@ -493,6 +534,7 @@ pub struct DhtState { worker_sender: UnboundedSender, pub(crate) peer_store: PeerStore, + announce_addr: Option, } impl DhtState { @@ -502,6 +544,7 @@ impl DhtState { routing_table: Option, listen_addr: SocketAddr, peer_store: PeerStore, + announce_addr: Option, ) -> Self { let routing_table = routing_table.unwrap_or_else(|| RoutingTable::new(id, None)); Self { @@ -513,6 +556,7 @@ impl DhtState { listen_addr, rate_limiter: make_rate_limiter(), peer_store, + announce_addr, } } @@ -581,6 +625,22 @@ impl DhtState { ip: None, kind: MessageKind::PingRequest(PingRequest { id: self.id }), }, + Request::Announce { + info_hash, + token, + addr, + } => Message { + kind: MessageKind::AnnouncePeer(AnnouncePeer { + id: self.id, + implied_port: 0, + info_hash, + port: addr.port(), + token, + }), + transaction_id: ByteString::from(transaction_id_buf.as_ref()), + version: None, + ip: None, + }, }; (transaction_id, message) } @@ -744,10 +804,15 @@ impl DhtState { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] enum Request { GetPeers(Id20), FindNode(Id20), + Announce { + info_hash: Id20, + token: ByteString, + addr: SocketAddrV4, + }, Ping, } @@ -1095,6 +1160,13 @@ impl DhtState { config.routing_table, listen_addr, config.peer_store.unwrap_or_else(|| PeerStore::new(peer_id)), + config.announce_addr.and_then(|a| match a { + SocketAddr::V4(v4) => Some(v4), + SocketAddr::V6(_) => { + warn!("libqrqbit-dht doesn't support announcing IPv6 addresses"); + None + } + }), )); spawn(error_span!("dht"), { From 6bb5d01c0f6f47e00634b0e73ea2e27584ad8bc9 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 21:17:37 +0000 Subject: [PATCH 11/28] Announcing port on DHT --- crates/dht/src/dht.rs | 28 ++++++++----------- crates/dht/src/persistence.rs | 4 +-- crates/librqbit/src/session.rs | 49 +++++----------------------------- 3 files changed, 19 insertions(+), 62 deletions(-) diff --git a/crates/dht/src/dht.rs b/crates/dht/src/dht.rs index 8a8863d..769d1de 100644 --- a/crates/dht/src/dht.rs +++ b/crates/dht/src/dht.rs @@ -1,6 +1,6 @@ use std::{ cmp::Reverse, - net::{SocketAddr, SocketAddrV4}, + net::SocketAddr, str::FromStr, sync::{ atomic::{AtomicU16, Ordering}, @@ -109,7 +109,7 @@ impl RecursiveRequestCallbacks for RecursiveRequestCallbacksGetPeers { addr: SocketAddr, resp: &anyhow::Result, ) { - let announce_addr = match req.dht.announce_addr { + let announce_port = match req.dht.announce_port { Some(a) => a, None => return, }; @@ -132,7 +132,7 @@ impl RecursiveRequestCallbacks for RecursiveRequestCallbacksGetPeers { let (tid, message) = req.dht.create_request(Request::Announce { info_hash: req.info_hash, token: token.clone(), - addr: announce_addr, + port: announce_port, }); let _ = req.dht.worker_sender.send(WorkerSendRequest { @@ -534,7 +534,7 @@ pub struct DhtState { worker_sender: UnboundedSender, pub(crate) peer_store: PeerStore, - announce_addr: Option, + announce_port: Option, } impl DhtState { @@ -544,7 +544,7 @@ impl DhtState { routing_table: Option, listen_addr: SocketAddr, peer_store: PeerStore, - announce_addr: Option, + announce_port: Option, ) -> Self { let routing_table = routing_table.unwrap_or_else(|| RoutingTable::new(id, None)); Self { @@ -556,7 +556,7 @@ impl DhtState { listen_addr, rate_limiter: make_rate_limiter(), peer_store, - announce_addr, + announce_port, } } @@ -628,13 +628,13 @@ impl DhtState { Request::Announce { info_hash, token, - addr, + port, } => Message { kind: MessageKind::AnnouncePeer(AnnouncePeer { id: self.id, implied_port: 0, info_hash, - port: addr.port(), + port, token, }), transaction_id: ByteString::from(transaction_id_buf.as_ref()), @@ -811,7 +811,7 @@ enum Request { Announce { info_hash: Id20, token: ByteString, - addr: SocketAddrV4, + port: u16, }, Ping, } @@ -1124,7 +1124,7 @@ pub struct DhtConfig { pub bootstrap_addrs: Option>, pub routing_table: Option, pub listen_addr: Option, - pub announce_addr: Option, + pub announce_port: Option, pub peer_store: Option, } @@ -1160,13 +1160,7 @@ impl DhtState { config.routing_table, listen_addr, config.peer_store.unwrap_or_else(|| PeerStore::new(peer_id)), - config.announce_addr.and_then(|a| match a { - SocketAddr::V4(v4) => Some(v4), - SocketAddr::V6(_) => { - warn!("libqrqbit-dht doesn't support announcing IPv6 addresses"); - None - } - }), + config.announce_port, )); spawn(error_span!("dht"), { diff --git a/crates/dht/src/persistence.rs b/crates/dht/src/persistence.rs index 1e39c05..2cb61b3 100644 --- a/crates/dht/src/persistence.rs +++ b/crates/dht/src/persistence.rs @@ -20,7 +20,7 @@ use crate::{Dht, DhtConfig, DhtState}; pub struct PersistentDhtConfig { pub dump_interval: Option, pub config_filename: Option, - pub announce_addr: Option, + pub announce_port: Option, } #[derive(Serialize, Deserialize)] @@ -118,7 +118,7 @@ impl PersistentDht { routing_table, listen_addr, peer_store, - announce_addr: config.announce_addr, + announce_port: config.announce_port, ..Default::default() }; let dht = DhtState::with_config(dht_config).await?; diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 8d7eba2..18f51dc 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -2,7 +2,7 @@ use std::{ borrow::Cow, collections::{HashMap, HashSet}, io::{BufReader, BufWriter, Read}, - net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + net::SocketAddr, path::PathBuf, str::FromStr, sync::Arc, @@ -357,34 +357,6 @@ async fn create_tcp_listener( bail!("no free TCP ports in range {port_range:?}"); } -async fn get_public_announce_addr(port: u16) -> anyhow::Result { - async fn get_ipify() -> anyhow::Result { - #[derive(Deserialize)] - struct Data { - ip: Ipv4Addr, - } - let resp: Data = reqwest::get("https://api.ipify.org?format=json") - .await - .context("error getting public IP address")? - .error_for_status()? - .json() - .await?; - Ok(resp.ip) - } - - async fn get_public_ip() -> anyhow::Result { - get_ipify().await - } - - let ip = get_public_ip() - .await - .context("error getting public IP address")?; - - let addr = SocketAddr::V4(SocketAddrV4::new(ip, port)); - info!("using public IP address {addr} to publish on DHT"); - Ok(addr) -} - pub(crate) struct CheckedIncomingConnection { pub addr: SocketAddr, pub stream: tokio::net::TcpStream, @@ -406,7 +378,7 @@ impl Session { ) -> anyhow::Result> { let peer_id = opts.peer_id.unwrap_or_else(generate_peer_id); - let (tcp_listener, port) = if let Some(port_range) = opts.listen_port_range { + let (tcp_listener, tcp_listen_port) = if let Some(port_range) = opts.listen_port_range { let (l, p) = create_tcp_listener(port_range) .await .context("error listening on TCP")?; @@ -419,24 +391,15 @@ impl Session { let dht = if opts.disable_dht { None } else { - let announce_addr = if let Some(port) = port { - Some( - get_public_announce_addr(port) - .await - .context("error getting public announce address")?, - ) - } else { - None - }; let dht = if opts.disable_dht_persistence { DhtBuilder::with_config(DhtConfig { - announce_addr, + announce_port: tcp_listen_port, ..Default::default() }) .await } else { let mut pdht_config = opts.dht_config.take().unwrap_or_default(); - pdht_config.announce_addr = announce_addr; + pdht_config.announce_port = tcp_listen_port; PersistentDht::create(Some(pdht_config)).await } .context("error initializing DHT")?; @@ -468,12 +431,12 @@ impl Session { if let Some(tcp_listener) = tcp_listener { session.spawn( "tcp listener", - error_span!("tcp_listen", port = port), + error_span!("tcp_listen", port = tcp_listen_port), session.clone().task_tcp_listener(tcp_listener), ); } - if let Some(listen_port) = port { + if let Some(listen_port) = tcp_listen_port { if opts.enable_upnp_port_forwarding { session.spawn( "upnp_forward", From 4d993aeb6decb8056381852a7b3cb218c58da611 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 21:30:35 +0000 Subject: [PATCH 12/28] Announcing works and peers connect to us! but havent seen nothing uploaded yet, it says they have it fully --- crates/dht/src/dht.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dht/src/dht.rs b/crates/dht/src/dht.rs index 769d1de..6268b8a 100644 --- a/crates/dht/src/dht.rs +++ b/crates/dht/src/dht.rs @@ -203,7 +203,7 @@ impl RequestPeersStream { node_tx, callbacks: RecursiveRequestCallbacksGetPeers { min_distance_to_announce: Id20::from_str( - "000000ffffffffffffffffffffffffffffffffff", + "0000ffffffffffffffffffffffffffffffffffff", ) .unwrap(), }, From bc43963ad2fd63bf9299b4feb2407fc07af530e6 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 21:39:19 +0000 Subject: [PATCH 13/28] noticed a bug, added to TODO --- TODO.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.md b/TODO.md index ac2c650..1282f7a 100644 --- a/TODO.md +++ b/TODO.md @@ -32,6 +32,9 @@ To do this, a - [x] Ensure that if we query the "returned" nodes, they are even closer to our request than the responding node id was. +incoming peers: +- [ ] error managing peer: expected extended handshake, but got Bitfield(<94 bytes>) + someday: - [x] cancellation from the client-side for the lib (i.e. stop the torrent manager) From 5e238419f4a3d9ef87f1dcb37cdf379c8bb695b9 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 22:14:55 +0000 Subject: [PATCH 14/28] Fix a bug with sending interested --- crates/librqbit/src/torrent_state/live/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 41083d6..4f7e5eb 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -1176,11 +1176,11 @@ impl PeerHandler { self.tx.send(WriterRequest::Disconnect)?; return Ok(()); } + } else { + self.tx + .send(WriterRequest::Message(MessageOwned::Interested))?; } - self.tx - .send(WriterRequest::Message(MessageOwned::Interested))?; - loop { self.wait_for_unchoke().await; From cc92afcdec91328afb1dbf4915c3603ab1146b78 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 22:19:58 +0000 Subject: [PATCH 15/28] Prevent self-connections just in case --- crates/librqbit/src/peer_connection.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/librqbit/src/peer_connection.rs b/crates/librqbit/src/peer_connection.rs index 33c3f2a..5605efe 100644 --- a/crates/librqbit/src/peer_connection.rs +++ b/crates/librqbit/src/peer_connection.rs @@ -3,7 +3,7 @@ use std::{ time::{Duration, Instant}, }; -use anyhow::Context; +use anyhow::{bail, Context}; use buffers::{ByteBuf, ByteString}; use clone_to_owned::CloneToOwned; use librqbit_core::{id20::Id20, lengths::ChunkInfo, peer_id::try_decode_peer_id}; @@ -142,6 +142,10 @@ impl PeerConnection { anyhow::bail!("wrong info hash"); } + if handshake.peer_id == self.peer_id.0 { + bail!("looks like we are connecting to ourselves"); + } + trace!( "incoming connection: id={:?}", try_decode_peer_id(Id20(handshake.peer_id)) @@ -217,6 +221,10 @@ impl PeerConnection { anyhow::bail!("info hash does not match"); } + if h.peer_id == self.peer_id.0 { + bail!("looks like we are connecting to ourselves"); + } + self.handler.on_handshake(h)?; if read_so_far > size { From b224ed2397fd7d9990d7c2741ef1d484d58384bb Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 22:22:04 +0000 Subject: [PATCH 16/28] Flush messages before disconnecting (lame) --- crates/librqbit/src/torrent_state/live/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 4f7e5eb..6b19825 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -1159,7 +1159,8 @@ impl PeerHandler { let handle = self.addr; self.wait_for_bitfield().await; - // TODO: this check needs to happen more often + // TODO: this check needs to happen more often, we need to update our + // interested state with the other side, for now we send it only once. if self.state.is_finished() { self.tx .send(WriterRequest::Message(MessageOwned::NotInterested))?; @@ -1174,6 +1175,8 @@ impl PeerHandler { { debug!("both peer and us have full torrent, disconnecting"); self.tx.send(WriterRequest::Disconnect)?; + // Sleep a bit to ensure this gets written to the network by manage_peer + tokio::time::sleep(Duration::from_millis(100)).await; return Ok(()); } } else { From 156f83dbee2fe8a99428db885c0aa3b7653f2106 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 22:24:22 +0000 Subject: [PATCH 17/28] Downgrade an error to debug --- crates/librqbit/src/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 18f51dc..fb3f2dc 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -564,7 +564,7 @@ impl Session { futs.push( self.check_incoming_connection(addr, stream) .map_err(|e| { - error!("error checking incoming connection: {e:#}"); + debug!("error checking incoming connection: {e:#}"); e }) .instrument(error_span!("incoming", addr=%addr)) From c3eb03c72dbdb0cf15b0e872d3aef48990d485ec Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 22:41:16 +0000 Subject: [PATCH 18/28] Show total uploaded in UI --- crates/librqbit/webui/src/rqbit-web.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/librqbit/webui/src/rqbit-web.tsx b/crates/librqbit/webui/src/rqbit-web.tsx index f9f7320..74950b5 100644 --- a/crates/librqbit/webui/src/rqbit-web.tsx +++ b/crates/librqbit/webui/src/rqbit-web.tsx @@ -199,11 +199,8 @@ const Speed: React.FC<{ statsResponse: TorrentStats }> = ({ statsResponse }) => return <> {!statsResponse.finished &&

↓ {statsResponse.live.download_speed.human_readable}

}

↑ {statsResponse.live.upload_speed.human_readable}

+ {statsResponse.live.snapshot.uploaded_bytes > 0 &&

Uploaded {formatBytes(statsResponse.live.snapshot.uploaded_bytes)}

} - - if (statsResponse.finished) { - return Completed; - } } const TorrentRow: React.FC<{ From bc243143e52c32eb9f852b57b09b205094c37c12 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 23:31:04 +0000 Subject: [PATCH 19/28] Do not announce when listing torrents --- crates/dht/examples/dht.rs | 2 +- crates/dht/src/dht.rs | 23 ++++++++++++++--------- crates/dht/src/persistence.rs | 2 -- crates/librqbit/src/dht_utils.rs | 2 +- crates/librqbit/src/session.rs | 24 ++++++++++++++---------- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/crates/dht/examples/dht.rs b/crates/dht/examples/dht.rs index 883ef79..11c289c 100644 --- a/crates/dht/examples/dht.rs +++ b/crates/dht/examples/dht.rs @@ -17,7 +17,7 @@ async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let dht = DhtBuilder::new().await.context("error initializing DHT")?; - let mut stream = dht.get_peers(info_hash)?; + let mut stream = dht.get_peers(info_hash, None)?; let stats_printer = async { loop { diff --git a/crates/dht/src/dht.rs b/crates/dht/src/dht.rs index 6268b8a..b55a54c 100644 --- a/crates/dht/src/dht.rs +++ b/crates/dht/src/dht.rs @@ -97,6 +97,7 @@ trait RecursiveRequestCallbacks: Sized + Send + Sync + 'static { struct RecursiveRequestCallbacksGetPeers { // Id20::from_str("00000fffffffffffffffffffffffffffffffffff").unwrap() min_distance_to_announce: Id20, + announce_port: Option, } impl RecursiveRequestCallbacks for RecursiveRequestCallbacksGetPeers { @@ -109,7 +110,7 @@ impl RecursiveRequestCallbacks for RecursiveRequestCallbacksGetPeers { addr: SocketAddr, resp: &anyhow::Result, ) { - let announce_port = match req.dht.announce_port { + let announce_port = match self.announce_port { Some(a) => a, None => return, }; @@ -189,7 +190,7 @@ pub struct RequestPeersStream { } impl RequestPeersStream { - fn new(dht: Arc, info_hash: Id20) -> Self { + fn new(dht: Arc, info_hash: Id20, announce_port: Option) -> Self { let (peer_tx, peer_rx) = unbounded_channel(); let (node_tx, node_rx) = unbounded_channel(); let rp = Arc::new(RecursiveRequest { @@ -206,6 +207,7 @@ impl RequestPeersStream { "0000ffffffffffffffffffffffffffffffffffff", ) .unwrap(), + announce_port, }, }); let join_handle = rp.request_peers_forever(node_rx); @@ -534,7 +536,6 @@ pub struct DhtState { worker_sender: UnboundedSender, pub(crate) peer_store: PeerStore, - announce_port: Option, } impl DhtState { @@ -544,7 +545,6 @@ impl DhtState { routing_table: Option, listen_addr: SocketAddr, peer_store: PeerStore, - announce_port: Option, ) -> Self { let routing_table = routing_table.unwrap_or_else(|| RoutingTable::new(id, None)); Self { @@ -556,7 +556,6 @@ impl DhtState { listen_addr, rate_limiter: make_rate_limiter(), peer_store, - announce_port, } } @@ -1124,7 +1123,6 @@ pub struct DhtConfig { pub bootstrap_addrs: Option>, pub routing_table: Option, pub listen_addr: Option, - pub announce_port: Option, pub peer_store: Option, } @@ -1160,7 +1158,6 @@ impl DhtState { config.routing_table, listen_addr, config.peer_store.unwrap_or_else(|| PeerStore::new(peer_id)), - config.announce_port, )); spawn(error_span!("dht"), { @@ -1174,8 +1171,16 @@ impl DhtState { Ok(state) } - pub fn get_peers(self: &Arc, info_hash: Id20) -> anyhow::Result { - Ok(RequestPeersStream::new(self.clone(), info_hash)) + pub fn get_peers( + self: &Arc, + info_hash: Id20, + announce_port: Option, + ) -> anyhow::Result { + Ok(RequestPeersStream::new( + self.clone(), + info_hash, + announce_port, + )) } pub fn listen_addr(&self) -> SocketAddr { diff --git a/crates/dht/src/persistence.rs b/crates/dht/src/persistence.rs index 2cb61b3..2c002bb 100644 --- a/crates/dht/src/persistence.rs +++ b/crates/dht/src/persistence.rs @@ -20,7 +20,6 @@ use crate::{Dht, DhtConfig, DhtState}; pub struct PersistentDhtConfig { pub dump_interval: Option, pub config_filename: Option, - pub announce_port: Option, } #[derive(Serialize, Deserialize)] @@ -118,7 +117,6 @@ impl PersistentDht { routing_table, listen_addr, peer_store, - announce_port: config.announce_port, ..Default::default() }; let dht = DhtState::with_config(dht_config).await?; diff --git a/crates/librqbit/src/dht_utils.rs b/crates/librqbit/src/dht_utils.rs index 455407d..ea1225b 100644 --- a/crates/librqbit/src/dht_utils.rs +++ b/crates/librqbit/src/dht_utils.rs @@ -108,7 +108,7 @@ mod tests { let info_hash = Id20::from_str("cab507494d02ebb1178b38f2e9d7be299c86b862").unwrap(); let dht = DhtBuilder::new().await.unwrap(); - let peer_rx = dht.get_peers(info_hash).unwrap(); + let peer_rx = dht.get_peers(info_hash, None).unwrap(); let peer_id = generate_peer_id(); match read_metainfo_from_peer_receiver(peer_id, info_hash, Vec::new(), peer_rx, None).await { diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index fb3f2dc..dcc9bd6 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -159,6 +159,8 @@ pub struct Session { db: RwLock, output_folder: PathBuf, + tcp_listen_port: Option, + cancel_tx: tokio::sync::watch::Sender<()>, cancel_rx: tokio::sync::watch::Receiver<()>, } @@ -392,14 +394,9 @@ impl Session { None } else { let dht = if opts.disable_dht_persistence { - DhtBuilder::with_config(DhtConfig { - announce_port: tcp_listen_port, - ..Default::default() - }) - .await + DhtBuilder::with_config(DhtConfig::default()).await } else { - let mut pdht_config = opts.dht_config.take().unwrap_or_default(); - pdht_config.announce_port = tcp_listen_port; + let pdht_config = opts.dht_config.take().unwrap_or_default(); PersistentDht::create(Some(pdht_config)).await } .context("error initializing DHT")?; @@ -426,6 +423,7 @@ impl Session { db: RwLock::new(Default::default()), cancel_rx, cancel_tx, + tcp_listen_port, }); if let Some(tcp_listener) = tcp_listener { @@ -740,6 +738,12 @@ impl Session { let opts = opts.unwrap_or_default(); + let announce_port = if opts.list_only { + None + } else { + self.tcp_listen_port + }; + let (info_hash, info, dht_rx, trackers, initial_peers) = match add { AddTorrent::Url(magnet) if magnet.starts_with("magnet:") => { let Magnet { @@ -751,7 +755,7 @@ impl Session { .dht .as_ref() .context("magnet links without DHT are not supported")? - .get_peers(info_hash)?; + .get_peers(info_hash, announce_port)?; let trackers = trackers .into_iter() @@ -814,7 +818,7 @@ impl Session { let dht_rx = match self.dht.as_ref() { Some(dht) if !opts.paused && !opts.list_only => { debug!("reading peers for {:?} from DHT", torrent.info_hash); - Some(dht.get_peers(torrent.info_hash)?) + Some(dht.get_peers(torrent.info_hash, announce_port)?) } _ => None, }; @@ -1047,7 +1051,7 @@ impl Session { let peer_rx = self .dht .as_ref() - .map(|dht| dht.get_peers(handle.info_hash())) + .map(|dht| dht.get_peers(handle.info_hash(), self.tcp_listen_port)) .transpose()?; handle.start(Default::default(), peer_rx, false)?; Ok(()) From ca8989f8e6355b18b06bf96ac991789895184506 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 23:24:24 +0000 Subject: [PATCH 20/28] Saving --- TODO.md | 1 + crates/librqbit/src/peer_connection.rs | 42 +++++++++---------- crates/librqbit/src/torrent_state/live/mod.rs | 2 +- .../src/extended/handshake.rs | 7 ++++ .../peer_binary_protocol/src/extended/mod.rs | 8 +--- crates/peer_binary_protocol/src/lib.rs | 12 +++--- 6 files changed, 37 insertions(+), 35 deletions(-) diff --git a/TODO.md b/TODO.md index 1282f7a..c9c555d 100644 --- a/TODO.md +++ b/TODO.md @@ -34,6 +34,7 @@ incoming peers: - [ ] error managing peer: expected extended handshake, but got Bitfield(<94 bytes>) +- [ ] do not announce when merely listing the torrent someday: - [x] cancellation from the client-side for the lib (i.e. stop the torrent manager) diff --git a/crates/librqbit/src/peer_connection.rs b/crates/librqbit/src/peer_connection.rs index 5605efe..4450d63 100644 --- a/crates/librqbit/src/peer_connection.rs +++ b/crates/librqbit/src/peer_connection.rs @@ -7,6 +7,7 @@ use anyhow::{bail, Context}; use buffers::{ByteBuf, ByteString}; use clone_to_owned::CloneToOwned; use librqbit_core::{id20::Id20, lengths::ChunkInfo, peer_id::try_decode_peer_id}; +use parking_lot::RwLock; use peer_binary_protocol::{ extended::{handshake::ExtendedHandshake, ExtendedMessage}, serialize_piece_preamble, Handshake, Message, MessageBorrowed, MessageDeserializeError, @@ -261,33 +262,19 @@ impl PeerConnection { .read_write_timeout .unwrap_or_else(|| Duration::from_secs(10)); - let mut extended_handshake: Option> = None; + let extended_handshake: RwLock>> = RwLock::new(None); + let extended_handshake_ref = &extended_handshake; let supports_extended = handshake_supports_extended; if supports_extended { let my_extended = Message::Extended(ExtendedMessage::Handshake(ExtendedHandshake::new())); trace!("sending extended handshake: {:?}", &my_extended); - my_extended.serialize(&mut write_buf, None).unwrap(); + my_extended.serialize(&mut write_buf, &|| None).unwrap(); with_timeout(rwtimeout, conn.write_all(&write_buf)) .await .context("error writing extended handshake")?; write_buf.clear(); - - let (extended, size) = read_one!(conn, read_buf, read_so_far, rwtimeout); - match extended { - Message::Extended(ExtendedMessage::Handshake(h)) => { - trace!("received: {:?}", &h); - self.handler.on_extended_handshake(&h)?; - extended_handshake = Some(h.clone_to_owned()) - } - other => anyhow::bail!("expected extended handshake, but got {:?}", other), - }; - - if read_so_far > size { - read_buf.copy_within(size..read_so_far, 0); - } - read_so_far -= size; } let (mut read_half, mut write_half) = tokio::io::split(conn); @@ -320,9 +307,12 @@ impl PeerConnection { let mut uploaded_add = None; let len = match &req { - WriterRequest::Message(msg) => { - msg.serialize(&mut write_buf, extended_handshake.as_ref())? - } + WriterRequest::Message(msg) => msg.serialize(&mut write_buf, &|| { + extended_handshake_ref + .read() + .as_ref() + .and_then(|e| e.ut_metadata()) + })?, WriterRequest::ReadChunkRequest(chunk) => { // this whole section is an optimization write_buf.resize(PIECE_MESSAGE_DEFAULT_LEN, 0); @@ -366,9 +356,15 @@ impl PeerConnection { let (message, size) = read_one!(read_half, read_buf, read_so_far, rwtimeout); trace!("received: {:?}", &message); - self.handler - .on_received_message(message) - .context("error in handler.on_received_message()")?; + if let Message::Extended(ExtendedMessage::Handshake(h)) = &message { + *extended_handshake_ref.write() = Some(h.clone_to_owned()); + self.handler.on_extended_handshake(h)?; + trace!("remembered extended handshake for future serializing"); + } else { + self.handler + .on_received_message(message) + .context("error in handler.on_received_message()")?; + } if read_so_far > size { read_buf.copy_within(size..read_so_far, 0); diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 6b19825..306661c 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -838,7 +838,7 @@ impl<'a> PeerConnectionHandler for &'a PeerHandler { fn serialize_bitfield_message_to_buf(&self, buf: &mut Vec) -> anyhow::Result { let g = self.state.lock_read("serialize_bitfield_message_to_buf"); let msg = Message::Bitfield(ByteBuf(g.get_chunks()?.get_have_pieces().as_raw_slice())); - let len = msg.serialize(buf, None)?; + let len = msg.serialize(buf, &|| None)?; trace!("sending: {:?}, length={}", &msg, len); Ok(len) } diff --git a/crates/peer_binary_protocol/src/extended/handshake.rs b/crates/peer_binary_protocol/src/extended/handshake.rs index db41803..62f0955 100644 --- a/crates/peer_binary_protocol/src/extended/handshake.rs +++ b/crates/peer_binary_protocol/src/extended/handshake.rs @@ -59,6 +59,13 @@ impl ExtendedHandshake { } }) } + + pub fn ut_metadata(&self) -> Option + where + ByteBuf: AsRef<[u8]>, + { + self.get_msgid(b"ut_metadata") + } } impl CloneToOwned for ExtendedHandshake diff --git a/crates/peer_binary_protocol/src/extended/mod.rs b/crates/peer_binary_protocol/src/extended/mod.rs index 47bb530..9ad8f32 100644 --- a/crates/peer_binary_protocol/src/extended/mod.rs +++ b/crates/peer_binary_protocol/src/extended/mod.rs @@ -1,7 +1,6 @@ use bencode::bencode_serialize_to_writer; use bencode::from_bytes; use bencode::BencodeValue; -use buffers::ByteString; use clone_to_owned::CloneToOwned; use serde::{Deserialize, Serialize}; @@ -41,7 +40,7 @@ impl<'a, ByteBuf: 'a + std::hash::Hash + Eq + Serialize> ExtendedMessage, - extended_handshake: Option<&ExtendedHandshake>, + extended_handshake_ut_metadata: &dyn Fn() -> Option, ) -> anyhow::Result<()> where ByteBuf: AsRef<[u8]>, @@ -56,12 +55,9 @@ impl<'a, ByteBuf: 'a + std::hash::Hash + Eq + Serialize> ExtendedMessage { - let h = extended_handshake.ok_or_else(|| { + let emsg_id = extended_handshake_ut_metadata().ok_or_else(|| { anyhow::anyhow!("need peer's handshake to serialize ut_metadata") })?; - let emsg_id = h - .get_msgid(b"ut_metadata") - .ok_or_else(|| anyhow::anyhow!("peer doesn't support ut_metadata"))?; out.push(emsg_id); u.serialize(out); } diff --git a/crates/peer_binary_protocol/src/lib.rs b/crates/peer_binary_protocol/src/lib.rs index 11171f7..b99edc5 100644 --- a/crates/peer_binary_protocol/src/lib.rs +++ b/crates/peer_binary_protocol/src/lib.rs @@ -11,7 +11,7 @@ use clone_to_owned::CloneToOwned; use librqbit_core::{constants::CHUNK_SIZE, id20::Id20, lengths::ChunkInfo}; use serde::{Deserialize, Serialize}; -use self::extended::{handshake::ExtendedHandshake, ExtendedMessage}; +use self::extended::ExtendedMessage; const INTEGER_LEN: usize = 4; const MSGID_LEN: usize = 1; @@ -258,7 +258,7 @@ where pub fn serialize( &self, out: &mut Vec, - peer_extended_handshake: Option<&ExtendedHandshake>, + extended_handshake_ut_metadata: &dyn Fn() -> Option, ) -> anyhow::Result { let (lp, msg_id) = self.len_prefix_and_msg_id(); @@ -308,7 +308,7 @@ where Ok(msg_len) } Message::Extended(e) => { - e.serialize(out, peer_extended_handshake)?; + e.serialize(out, extended_handshake_ut_metadata)?; let msg_size = out.len(); // no fucking idea why +1, but I tweaked that for it all to match up // with real messages. @@ -576,6 +576,8 @@ impl Request { #[cfg(test)] mod tests { + use crate::extended::handshake::ExtendedHandshake; + use super::*; #[test] fn test_handshake_serialize() { @@ -594,7 +596,7 @@ mod tests { fn test_extended_serialize() { let msg = Message::Extended(ExtendedMessage::Handshake(ExtendedHandshake::new())); let mut out = Vec::new(); - msg.serialize(&mut out, None).unwrap(); + msg.serialize(&mut out, &|| None).unwrap(); dbg!(out); } @@ -610,7 +612,7 @@ mod tests { let (msg, size) = MessageBorrowed::deserialize(&buf).unwrap(); assert_eq!(size, buf.len()); let mut write_buf = Vec::new(); - msg.serialize(&mut write_buf, None).unwrap(); + msg.serialize(&mut write_buf, &|| None).unwrap(); if buf != write_buf { { use std::io::Write; From a73e0e675f1fe955313216f77887e352d34425ea Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 23:44:29 +0000 Subject: [PATCH 21/28] tweak peer_connection log message --- crates/librqbit/src/peer_connection.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/librqbit/src/peer_connection.rs b/crates/librqbit/src/peer_connection.rs index 4450d63..9a429bf 100644 --- a/crates/librqbit/src/peer_connection.rs +++ b/crates/librqbit/src/peer_connection.rs @@ -330,6 +330,7 @@ impl PeerConnection { full_len } WriterRequest::Disconnect => { + trace!("disconnect requested, closing writer"); return Ok(()); } }; @@ -378,10 +379,15 @@ impl PeerConnection { }; let r = tokio::select! { - r = reader => {r} - r = writer => {r} + r = reader => { + trace!("reader is done, exiting"); + r + } + r = writer => { + trace!("writer is done, exiting"); + r + } }; - trace!("either reader or writer are done, exiting"); r } } From 124be19e433206b852bf6fd4ea7d716c5f9fdd09 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Dec 2023 23:58:47 +0000 Subject: [PATCH 22/28] Format upload speed --- crates/librqbit/webui/src/rqbit-web.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/librqbit/webui/src/rqbit-web.tsx b/crates/librqbit/webui/src/rqbit-web.tsx index 74950b5..8dd4644 100644 --- a/crates/librqbit/webui/src/rqbit-web.tsx +++ b/crates/librqbit/webui/src/rqbit-web.tsx @@ -197,9 +197,12 @@ const Speed: React.FC<{ statsResponse: TorrentStats }> = ({ statsResponse }) => } return <> - {!statsResponse.finished &&

↓ {statsResponse.live.download_speed.human_readable}

} -

↑ {statsResponse.live.upload_speed.human_readable}

- {statsResponse.live.snapshot.uploaded_bytes > 0 &&

Uploaded {formatBytes(statsResponse.live.snapshot.uploaded_bytes)}

} + {!statsResponse.finished && +
↓ {statsResponse.live.download_speed.human_readable}
} +
+ ↑ {statsResponse.live.upload_speed.human_readable} + {statsResponse.live.snapshot.uploaded_bytes > 0 && + (total {formatBytes(statsResponse.live.snapshot.uploaded_bytes)}})
} From fd17ddc46b70837759f16981c0214748b1344ce6 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Wed, 6 Dec 2023 00:16:59 +0000 Subject: [PATCH 23/28] Allow peers to reconnect --- crates/librqbit/src/torrent_state/live/mod.rs | 21 ++++++++++++++++--- .../src/torrent_state/live/peer/mod.rs | 19 +++++++++++++++++ .../torrent_state/live/peer/stats/atomic.rs | 5 +++-- .../torrent_state/live/peer/stats/snapshot.rs | 8 +++++-- crates/rqbit/src/main.rs | 2 +- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 306661c..c2b4ef1 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -382,7 +382,17 @@ impl TorrentStateLive { let (tx, rx) = unbounded_channel(); let counters = match self.peers.states.entry(checked_peer.addr) { - Entry::Occupied(_) => bail!("we are already managing peer {}", checked_peer.addr), + Entry::Occupied(mut occ) => { + let peer = occ.get_mut(); + peer.state + .incoming_connection( + Id20(checked_peer.handshake.peer_id), + tx.clone(), + &self.peers.stats, + ) + .context("peer already existed")?; + peer.stats.counters.clone() + } Entry::Vacant(vac) => { let peer = Peer::new_live_for_incoming_connection( Id20(checked_peer.handshake.peer_id), @@ -394,6 +404,9 @@ impl TorrentStateLive { counters } }; + counters + .incoming_connections + .fetch_add(1, Ordering::Relaxed); self.spawn( "incoming peer", @@ -504,7 +517,7 @@ impl TorrentStateLive { handler .counters - .connection_attempts + .outgoing_connection_attempts .fetch_add(1, Ordering::Relaxed); let res = tokio::select! { r = requester => {r} @@ -803,7 +816,9 @@ struct PeerHandler { impl<'a> PeerConnectionHandler for &'a PeerHandler { fn on_connected(&self, connection_time: Duration) { - self.counters.connections.fetch_add(1, Ordering::Relaxed); + self.counters + .outgoing_connections + .fetch_add(1, Ordering::Relaxed); self.counters .total_time_connecting_ms .fetch_add(connection_time.as_millis() as u64, Ordering::Relaxed); diff --git a/crates/librqbit/src/torrent_state/live/peer/mod.rs b/crates/librqbit/src/torrent_state/live/peer/mod.rs index 37cfce4..a915999 100644 --- a/crates/librqbit/src/torrent_state/live/peer/mod.rs +++ b/crates/librqbit/src/torrent_state/live/peer/mod.rs @@ -135,6 +135,25 @@ impl PeerStateNoMut { None } } + + pub fn incoming_connection( + &mut self, + peer_id: Id20, + tx: PeerTx, + counters: &AggregatePeerStatsAtomic, + ) -> anyhow::Result<()> { + if matches!(&self.0, PeerState::Connecting(..) | PeerState::Live(..)) { + anyhow::bail!("peer already active"); + } + match self.take(counters) { + PeerState::Queued | PeerState::Dead | PeerState::NotNeeded => { + self.set(PeerState::Live(LivePeerState::new(peer_id, tx)), counters); + } + PeerState::Connecting(..) | PeerState::Live(..) => unreachable!(), + } + Ok(()) + } + pub fn connecting_to_live( &mut self, peer_id: Id20, diff --git a/crates/librqbit/src/torrent_state/live/peer/stats/atomic.rs b/crates/librqbit/src/torrent_state/live/peer/stats/atomic.rs index 6c9b80a..2933d07 100644 --- a/crates/librqbit/src/torrent_state/live/peer/stats/atomic.rs +++ b/crates/librqbit/src/torrent_state/live/peer/stats/atomic.rs @@ -12,8 +12,9 @@ use backoff::{ExponentialBackoff, ExponentialBackoffBuilder}; pub(crate) struct PeerCountersAtomic { pub fetched_bytes: AtomicU64, pub total_time_connecting_ms: AtomicU64, - pub connection_attempts: AtomicU32, - pub connections: AtomicU32, + pub incoming_connections: AtomicU32, + pub outgoing_connection_attempts: AtomicU32, + pub outgoing_connections: AtomicU32, pub errors: AtomicU32, pub fetched_chunks: AtomicU32, pub downloaded_and_checked_pieces: AtomicU32, diff --git a/crates/librqbit/src/torrent_state/live/peer/stats/snapshot.rs b/crates/librqbit/src/torrent_state/live/peer/stats/snapshot.rs index 48db933..df18007 100644 --- a/crates/librqbit/src/torrent_state/live/peer/stats/snapshot.rs +++ b/crates/librqbit/src/torrent_state/live/peer/stats/snapshot.rs @@ -6,6 +6,7 @@ use crate::torrent_state::live::peer::{Peer, PeerState}; #[derive(Serialize, Deserialize)] pub struct PeerCounters { + pub incoming_connections: u32, pub fetched_bytes: u64, pub total_time_connecting_ms: u64, pub connection_attempts: u32, @@ -24,10 +25,13 @@ pub struct PeerStats { impl From<&super::atomic::PeerCountersAtomic> for PeerCounters { fn from(counters: &super::atomic::PeerCountersAtomic) -> Self { Self { + incoming_connections: counters.incoming_connections.load(Ordering::Relaxed), fetched_bytes: counters.fetched_bytes.load(Ordering::Relaxed), total_time_connecting_ms: counters.total_time_connecting_ms.load(Ordering::Relaxed), - connection_attempts: counters.connection_attempts.load(Ordering::Relaxed), - connections: counters.connections.load(Ordering::Relaxed), + connection_attempts: counters + .outgoing_connection_attempts + .load(Ordering::Relaxed), + connections: counters.outgoing_connections.load(Ordering::Relaxed), errors: counters.errors.load(Ordering::Relaxed), fetched_chunks: counters.fetched_chunks.load(Ordering::Relaxed), downloaded_and_checked_pieces: counters diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index 72168af..3049c73 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -428,7 +428,7 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> { http_api .make_http_api_and_run(http_api_listen_addr, false) .await - .context("error starting HTTP API") + .context("error running HTTP API") } }, SubCommand::Download(download_opts) => { From 91873ed2879548c65bbfaab9bb661ab3ded92e54 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Wed, 6 Dec 2023 00:26:52 +0000 Subject: [PATCH 24/28] Bump seen counters --- crates/librqbit/src/torrent_state/live/mod.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index c2b4ef1..1f26493 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -91,7 +91,7 @@ use crate::{ }, session::CheckedIncomingConnection, spawn_utils::spawn, - torrent_state::peer::Peer, + torrent_state::{peer::Peer, utils::atomic_inc}, tracker_comms::{TrackerError, TrackerRequest, TrackerRequestEvent, TrackerResponse}, type_aliases::{PeerHandle, BF}, }; @@ -394,6 +394,7 @@ impl TorrentStateLive { peer.stats.counters.clone() } Entry::Vacant(vac) => { + atomic_inc(&self.peers.stats.seen); let peer = Peer::new_live_for_incoming_connection( Id20(checked_peer.handshake.peer_id), tx.clone(), @@ -404,9 +405,7 @@ impl TorrentStateLive { counters } }; - counters - .incoming_connections - .fetch_add(1, Ordering::Relaxed); + atomic_inc(&counters.incoming_connections); self.spawn( "incoming peer", From 0cd875e740911377aba147a4e23fb90ee42fe25a Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Wed, 6 Dec 2023 00:39:52 +0000 Subject: [PATCH 25/28] Incoming peers now respect concurrency limits --- crates/librqbit/src/torrent_state/live/mod.rs | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index 1f26493..5557c4a 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -76,7 +76,7 @@ use sha1w::Sha1; use tokio::{ sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, - Notify, Semaphore, + Notify, OwnedSemaphorePermit, Semaphore, }, time::timeout, }; @@ -178,7 +178,7 @@ pub struct TorrentStateLive { lengths: Lengths, // Limits how many active (occupying network resources) peers there are at a moment in time. - peer_semaphore: Semaphore, + peer_semaphore: Arc, // The queue for peer manager to connect to them. peer_queue_tx: UnboundedSender, @@ -224,7 +224,7 @@ impl TorrentStateLive { }, initially_needed_bytes: needed_bytes, lengths, - peer_semaphore: Semaphore::new(128), + peer_semaphore: Arc::new(Semaphore::new(128)), peer_queue_tx, finished_notify: Notify::new(), down_speed_estimator, @@ -380,6 +380,16 @@ impl TorrentStateLive { ) -> anyhow::Result<()> { use dashmap::mapref::entry::Entry; let (tx, rx) = unbounded_channel(); + let permit = match self.peer_semaphore.clone().try_acquire_owned() { + Ok(permit) => permit, + Err(_) => { + warn!("limit of live peers reached, dropping incoming peer"); + self.peers.with_peer(checked_peer.addr, |p| { + atomic_inc(&p.stats.counters.incoming_connections); + }); + return Ok(()); + } + }; let counters = match self.peers.states.entry(checked_peer.addr) { Entry::Occupied(mut occ) => { @@ -411,7 +421,7 @@ impl TorrentStateLive { "incoming peer", error_span!("manage_incoming_peer", addr = %checked_peer.addr), self.clone() - .task_manage_incoming_peer(checked_peer, counters, tx, rx), + .task_manage_incoming_peer(checked_peer, counters, tx, rx, permit), ); Ok(()) } @@ -422,6 +432,7 @@ impl TorrentStateLive { counters: Arc, tx: PeerTx, rx: PeerRx, + permit: OwnedSemaphorePermit, ) -> anyhow::Result<()> { // TODO: bump counters for incoming let handler = PeerHandler { @@ -463,8 +474,6 @@ impl TorrentStateLive { ) => {r} }; - handler.state.peer_semaphore.add_permits(1); - match res { // We disconnected the peer ourselves as we don't need it Ok(()) => { @@ -475,10 +484,15 @@ impl TorrentStateLive { handler.on_peer_died(Some(e))?; } }; + drop(permit); Ok(()) } - async fn task_manage_outgoing_peer(self: Arc, addr: SocketAddr) -> anyhow::Result<()> { + async fn task_manage_outgoing_peer( + self: Arc, + addr: SocketAddr, + permit: OwnedSemaphorePermit, + ) -> anyhow::Result<()> { let state = self; let (rx, tx) = state.peers.mark_peer_connecting(addr)?; let counters = state @@ -523,8 +537,6 @@ impl TorrentStateLive { r = peer_connection.manage_peer_outgoing(rx) => {r} }; - handler.state.peer_semaphore.add_permits(1); - match res { // We disconnected the peer ourselves as we don't need it Ok(()) => { @@ -535,6 +547,7 @@ impl TorrentStateLive { handler.on_peer_died(Some(e))?; } } + drop(permit); Ok::<_, anyhow::Error>(()) } @@ -551,12 +564,11 @@ impl TorrentStateLive { continue; } - let permit = state.peer_semaphore.acquire().await?; - permit.forget(); + let permit = state.peer_semaphore.clone().acquire_owned().await?; state.spawn( "manage_peer", error_span!(parent: state.meta.span.clone(), "manage_peer", peer = addr.to_string()), - state.clone().task_manage_outgoing_peer(addr), + state.clone().task_manage_outgoing_peer(addr, permit), ); } } From 4caed4f50db03fabbb488eca3fbe5ec25efded0f Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Wed, 6 Dec 2023 00:46:15 +0000 Subject: [PATCH 26/28] fix a bug in jsx --- crates/librqbit/webui/src/rqbit-web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/librqbit/webui/src/rqbit-web.tsx b/crates/librqbit/webui/src/rqbit-web.tsx index 8dd4644..0617ffc 100644 --- a/crates/librqbit/webui/src/rqbit-web.tsx +++ b/crates/librqbit/webui/src/rqbit-web.tsx @@ -202,7 +202,7 @@ const Speed: React.FC<{ statsResponse: TorrentStats }> = ({ statsResponse }) =>
↑ {statsResponse.live.upload_speed.human_readable} {statsResponse.live.snapshot.uploaded_bytes > 0 && - (total {formatBytes(statsResponse.live.snapshot.uploaded_bytes)}})
+ ({formatBytes(statsResponse.live.snapshot.uploaded_bytes)})} } From 303c8a271df3efbe4299e0d4b9bc79ae108c6e17 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Wed, 6 Dec 2023 01:00:45 +0000 Subject: [PATCH 27/28] Shorten command line output --- crates/rqbit/src/main.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index 3049c73..c81f50d 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -377,7 +377,8 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> { None => continue }; let stats = handle.stats_snapshot(); - let speed = handle.down_speed_estimator(); + let down_speed = handle.down_speed_estimator(); + let up_speed = handle.up_speed_estimator(); let total = stats.total_bytes; let progress = stats.total_bytes - stats.remaining_bytes; let downloaded_pct = if stats.remaining_bytes == 0 { @@ -385,20 +386,23 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> { } else { (progress as f64 / total as f64) * 100f64 }; + let time_remaining = down_speed.time_remaining(); + let eta = match &time_remaining { + Some(d) => format!(", ETA: {:?}", d), + None => String::new() + }; info!( - "[{}]: {:.2}% ({:.2}), down speed {:.2} MiB/s, fetched {}, remaining {:.2} of {:.2}, uploaded {:.2}, peers: {{live: {}, connecting: {}, queued: {}, seen: {}, dead: {}}}", + "[{}]: {:.2}% ({:.2} / {:.2}), ↓{:.2} MiB/s, ↑{:.2} MiB/s ({:.2}){}, {{live: {}, queued: {}, dead: {}}}", idx, downloaded_pct, SF::new(progress), - speed.mbps(), - SF::new(stats.fetched_bytes), - SF::new(stats.remaining_bytes), SF::new(total), + down_speed.mbps(), + up_speed.mbps(), SF::new(stats.uploaded_bytes), - stats.peer_stats.live, - stats.peer_stats.connecting, + eta, + stats.peer_stats.live + stats.peer_stats.connecting, stats.peer_stats.queued, - stats.peer_stats.seen, stats.peer_stats.dead, ); } From 14d7ee04eae9996e44855a55f3a574b5a797a226 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Wed, 6 Dec 2023 01:18:25 +0000 Subject: [PATCH 28/28] Prepare for publishing new crates --- Cargo.lock | 6 +++--- crates/dht/Cargo.toml | 2 +- crates/librqbit/Cargo.toml | 6 +++--- crates/librqbit_core/Cargo.toml | 2 +- crates/peer_binary_protocol/Cargo.toml | 2 +- crates/rqbit/Cargo.toml | 4 ++-- crates/upnp/Cargo.toml | 6 ++++++ crates/upnp/README.md | 1 + 8 files changed, 18 insertions(+), 11 deletions(-) create mode 120000 crates/upnp/README.md diff --git a/Cargo.lock b/Cargo.lock index be8d3ba..1b88261 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1253,7 +1253,7 @@ dependencies = [ [[package]] name = "librqbit" -version = "4.1.0" +version = "5.0.0-beta.0" dependencies = [ "anyhow", "axum 0.7.1", @@ -1323,7 +1323,7 @@ version = "2.2.1" [[package]] name = "librqbit-core" -version = "3.2.1" +version = "3.3.0" dependencies = [ "anyhow", "directories", @@ -2002,7 +2002,7 @@ dependencies = [ [[package]] name = "rqbit" -version = "4.1.0" +version = "5.0.0-beta.0" dependencies = [ "anyhow", "clap", diff --git a/crates/dht/Cargo.toml b/crates/dht/Cargo.toml index f5da4e5..e3a55b3 100644 --- a/crates/dht/Cargo.toml +++ b/crates/dht/Cargo.toml @@ -34,7 +34,7 @@ indexmap = "2" dashmap = {version = "5.5.3", features = ["serde"]} clone_to_owned = {path="../clone_to_owned", package="librqbit-clone-to-owned", version = "2.2.1"} -librqbit-core = {path="../librqbit_core", version = "3.2.1"} +librqbit-core = {path="../librqbit_core", version = "3.3.0"} chrono = {version = "0.4.31", features = ["serde"]} [dev-dependencies] diff --git a/crates/librqbit/Cargo.toml b/crates/librqbit/Cargo.toml index 3f4afa8..e79e223 100644 --- a/crates/librqbit/Cargo.toml +++ b/crates/librqbit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librqbit" -version = "4.1.0" +version = "5.0.0-beta.0" authors = ["Igor Katson "] edition = "2021" description = "The main library used by rqbit torrent client. The binary is just a small wrapper on top of it." @@ -24,11 +24,11 @@ rust-tls = ["reqwest/rustls-tls"] [dependencies] bencode = {path = "../bencode", default-features=false, package="librqbit-bencode", version="2.2.1"} buffers = {path = "../buffers", package="librqbit-buffers", version = "2.2.1"} -librqbit-core = {path = "../librqbit_core", version = "3.2.1"} +librqbit-core = {path = "../librqbit_core", version = "3.3.0"} clone_to_owned = {path = "../clone_to_owned", package="librqbit-clone-to-owned", version = "2.2.1"} peer_binary_protocol = {path = "../peer_binary_protocol", package="librqbit-peer-protocol", version = "3.3.0"} sha1w = {path = "../sha1w", default-features=false, package="librqbit-sha1-wrapper", version="2.2.1"} -dht = {path = "../dht", package="librqbit-dht", version="4.0.0"} +dht = {path = "../dht", package="librqbit-dht", version="4.1.0"} librqbit-upnp = {path = "../upnp", version = "0.1.0"} tokio = {version = "1", features = ["macros", "rt-multi-thread"]} diff --git a/crates/librqbit_core/Cargo.toml b/crates/librqbit_core/Cargo.toml index 9d0d85e..0ddd412 100644 --- a/crates/librqbit_core/Cargo.toml +++ b/crates/librqbit_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librqbit-core" -version = "3.2.1" +version = "3.3.0" edition = "2021" description = "Important utilities used throughout librqbit useful for working with torrents." license = "Apache-2.0" diff --git a/crates/peer_binary_protocol/Cargo.toml b/crates/peer_binary_protocol/Cargo.toml index 8261b54..4dae265 100644 --- a/crates/peer_binary_protocol/Cargo.toml +++ b/crates/peer_binary_protocol/Cargo.toml @@ -23,6 +23,6 @@ byteorder = "1" buffers = {path="../buffers", package="librqbit-buffers", version = "2.2.1"} bencode = {path = "../bencode", default-features=false, package="librqbit-bencode", version="2.2.1"} clone_to_owned = {path="../clone_to_owned", package="librqbit-clone-to-owned", version = "2.2.1"} -librqbit-core = {path="../librqbit_core", version = "3.2.1"} +librqbit-core = {path="../librqbit_core", version = "3.3.0"} bitvec = "1" anyhow = "1" \ No newline at end of file diff --git a/crates/rqbit/Cargo.toml b/crates/rqbit/Cargo.toml index 741a1da..00b6fd3 100644 --- a/crates/rqbit/Cargo.toml +++ b/crates/rqbit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rqbit" -version = "4.1.0" +version = "5.0.0-beta.0" authors = ["Igor Katson "] edition = "2021" description = "A bittorrent command line client and server." @@ -23,7 +23,7 @@ default-tls = ["librqbit/default-tls"] rust-tls = ["librqbit/rust-tls"] [dependencies] -librqbit = {path="../librqbit", default-features=false, version = "4.1.0"} +librqbit = {path="../librqbit", default-features=false, version = "5.0.0-beta.0"} tokio = {version = "1", features = ["macros", "rt-multi-thread"]} console-subscriber = {version = "0.2", optional = true} anyhow = "1" diff --git a/crates/upnp/Cargo.toml b/crates/upnp/Cargo.toml index 07b5cc1..098a576 100644 --- a/crates/upnp/Cargo.toml +++ b/crates/upnp/Cargo.toml @@ -1,7 +1,13 @@ [package] name = "librqbit-upnp" version = "0.1.0" +authors = ["Igor Katson "] edition = "2021" +description = "Library used by rqbit torrent client to lease port forwards on the router." +license = "Apache-2.0" +documentation = "https://docs.rs/librqbit-upnp" +repository = "https://github.com/ikatson/rqbit" +readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/upnp/README.md b/crates/upnp/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/crates/upnp/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file