chore: formatting
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::trace;
|
use crate::trace;
|
||||||
@@ -161,7 +161,10 @@ impl ActorSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trace!("[actor_system] run_actor_loop ENDED - processed {} messages, {} ticks", message_count, tick_count);
|
trace!(
|
||||||
|
"[actor_system] run_actor_loop ENDED - processed {} messages, {} ticks",
|
||||||
|
message_count, tick_count
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,9 +159,12 @@ impl PeerActor {
|
|||||||
if let Some(ref handle) = self.self_handle
|
if let Some(ref handle) = self.self_handle
|
||||||
&& let Err(e) =
|
&& let Err(e) =
|
||||||
handle.send(PeerMessage::SendMessage(transfer_response))
|
handle.send(PeerMessage::SendMessage(transfer_response))
|
||||||
{
|
{
|
||||||
error!("[peer:{}] Failed to send TransferResponse message: {}", username, e);
|
error!(
|
||||||
}
|
"[peer:{}] Failed to send TransferResponse message: {}",
|
||||||
|
username, e
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PeerMessage::TransferResponse {
|
PeerMessage::TransferResponse {
|
||||||
token,
|
token,
|
||||||
@@ -177,15 +180,15 @@ impl PeerActor {
|
|||||||
if !allowed {
|
if !allowed {
|
||||||
if let Some(reason_text) = reason {
|
if let Some(reason_text) = reason {
|
||||||
debug!(
|
debug!(
|
||||||
"[peer:{}] Transfer rejected: {} - token {}, waiting for TransferRequest...",
|
"[peer:{}] Transfer rejected: {} - token {}, waiting for TransferRequest...",
|
||||||
username, reason_text, token
|
username, reason_text, token
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
"[peer:{}] Transfer allowed, ready to connect with token {:}",
|
"[peer:{}] Transfer allowed, ready to connect with token {:}",
|
||||||
username, token
|
username, token
|
||||||
);
|
);
|
||||||
self.client_channel
|
self.client_channel
|
||||||
.send(ClientOperation::DownloadFromPeer(
|
.send(ClientOperation::DownloadFromPeer(
|
||||||
token,
|
token,
|
||||||
@@ -258,7 +261,9 @@ impl PeerActor {
|
|||||||
let username = self.peer.read().unwrap().username.clone();
|
let username = self.peer.read().unwrap().username.clone();
|
||||||
error!(
|
error!(
|
||||||
"[peer:{}] Error reading from peer: {} (kind: {:?}). Disconnecting.",
|
"[peer:{}] Error reading from peer: {} (kind: {:?}). Disconnecting.",
|
||||||
username, e, e.kind()
|
username,
|
||||||
|
e,
|
||||||
|
e.kind()
|
||||||
);
|
);
|
||||||
self.disconnect_with_error(e);
|
self.disconnect_with_error(e);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use std::sync::mpsc::{Receiver, Sender};
|
|||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::{debug, error, info, trace, warn, SoulseekRs};
|
use crate::{SoulseekRs, debug, error, info, trace, warn};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PeerAddress {
|
pub struct PeerAddress {
|
||||||
@@ -483,8 +483,8 @@ impl ServerActor {
|
|||||||
obfuscated_port,
|
obfuscated_port,
|
||||||
} => {
|
} => {
|
||||||
debug!(
|
debug!(
|
||||||
"[server] Received GetPeerAddress response for {}: {}:{} (obf_type: {}, obf_port: {})"
|
"[server] Received GetPeerAddress response for {}: {}:{} (obf_type: {}, obf_port: {})",
|
||||||
, username, host, port, obfuscation_type, obfuscated_port
|
username, host, port, obfuscation_type, obfuscated_port
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = self.client_channel.send(
|
if let Err(e) = self.client_channel.send(
|
||||||
@@ -496,7 +496,10 @@ impl ServerActor {
|
|||||||
obfuscated_port,
|
obfuscated_port,
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
error!("[server] Error forwarding GetPeerAddress response to client: {}", e);
|
error!(
|
||||||
|
"[server] Error forwarding GetPeerAddress response to client: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ServerMessage::ProcessRead => {
|
ServerMessage::ProcessRead => {
|
||||||
@@ -515,23 +518,27 @@ impl ServerActor {
|
|||||||
let timeout = Duration::from_secs(5);
|
let timeout = Duration::from_secs(5);
|
||||||
|
|
||||||
let context = self.context.clone();
|
let context = self.context.clone();
|
||||||
std::thread::spawn(move || loop {
|
std::thread::spawn(move || {
|
||||||
if start.elapsed() >= timeout {
|
loop {
|
||||||
let _ = response.send(Err(SoulseekRs::Timeout));
|
if start.elapsed() >= timeout {
|
||||||
break;
|
let _ = response.send(Err(SoulseekRs::Timeout));
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(logged_in) = context.read().unwrap().logged_in {
|
if let Some(logged_in) =
|
||||||
let result = if logged_in {
|
context.read().unwrap().logged_in
|
||||||
Ok(true)
|
{
|
||||||
} else {
|
let result = if logged_in {
|
||||||
Err(SoulseekRs::AuthenticationFailed)
|
Ok(true)
|
||||||
};
|
} else {
|
||||||
let _ = response.send(result);
|
Err(SoulseekRs::AuthenticationFailed)
|
||||||
break;
|
};
|
||||||
}
|
let _ = response.send(result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ServerMessage::FileSearch { token, query } => {
|
ServerMessage::FileSearch { token, query } => {
|
||||||
@@ -560,7 +567,8 @@ impl ServerActor {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
"[server] Error reading from server: {} (kind: {:?}). Disconnecting.",
|
"[server] Error reading from server: {} (kind: {:?}). Disconnecting.",
|
||||||
e, e.kind()
|
e,
|
||||||
|
e.kind()
|
||||||
);
|
);
|
||||||
self.disconnect_with_error(e);
|
self.disconnect_with_error(e);
|
||||||
return;
|
return;
|
||||||
|
|||||||
+336
-290
@@ -1,27 +1,27 @@
|
|||||||
use crate::actor::server_actor::{PeerAddress, ServerActor, ServerMessage};
|
|
||||||
use crate::actor::ActorHandle;
|
use crate::actor::ActorHandle;
|
||||||
|
use crate::actor::server_actor::{PeerAddress, ServerActor, ServerMessage};
|
||||||
use crate::types::DownloadStatus;
|
use crate::types::DownloadStatus;
|
||||||
use crate::utils::logger;
|
use crate::utils::logger;
|
||||||
use crate::{
|
use crate::{
|
||||||
actor::{peer_registry::PeerRegistry, ActorSystem},
|
Transfer,
|
||||||
|
actor::{ActorSystem, peer_registry::PeerRegistry},
|
||||||
error::{Result, SoulseekRs},
|
error::{Result, SoulseekRs},
|
||||||
peer::{listen::Listen, ConnectionType, DownloadPeer, NewPeer, Peer},
|
peer::{ConnectionType, DownloadPeer, NewPeer, Peer, listen::Listen},
|
||||||
types::{Download, Search, SearchResult},
|
types::{Download, Search, SearchResult},
|
||||||
utils::{md5, thread_pool::ThreadPool},
|
utils::{md5, thread_pool::ThreadPool},
|
||||||
Transfer,
|
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
net::TcpStream,
|
net::TcpStream,
|
||||||
sync::{
|
sync::{
|
||||||
|
RwLock,
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
mpsc::{Receiver, Sender},
|
mpsc::{Receiver, Sender},
|
||||||
RwLock,
|
|
||||||
},
|
},
|
||||||
thread::{self, sleep},
|
thread::{self, sleep},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
sync::{mpsc, Arc},
|
sync::{Arc, mpsc},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -286,9 +286,10 @@ impl Client {
|
|||||||
pub fn remove_peer(&self, username: &str) {
|
pub fn remove_peer(&self, username: &str) {
|
||||||
let context = self.context.read().unwrap();
|
let context = self.context.read().unwrap();
|
||||||
if let Some(ref registry) = context.peer_registry
|
if let Some(ref registry) = context.peer_registry
|
||||||
&& let Some(handle) = registry.remove_peer(username) {
|
&& let Some(handle) = registry.remove_peer(username)
|
||||||
let _ = handle.stop();
|
{
|
||||||
}
|
let _ = handle.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn search(
|
pub fn search(
|
||||||
@@ -334,10 +335,11 @@ impl Client {
|
|||||||
|
|
||||||
// Check if cancelled
|
// Check if cancelled
|
||||||
if let Some(ref flag) = cancel_flag
|
if let Some(ref flag) = cancel_flag
|
||||||
&& flag.load(Ordering::Relaxed) {
|
&& flag.load(Ordering::Relaxed)
|
||||||
info!("Search cancelled by user");
|
{
|
||||||
break;
|
info!("Search cancelled by user");
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if timeout reached
|
// Check if timeout reached
|
||||||
if start.elapsed() >= timeout {
|
if start.elapsed() >= timeout {
|
||||||
@@ -470,225 +472,12 @@ impl Client {
|
|||||||
client_context: Arc<RwLock<ClientContext>>,
|
client_context: Arc<RwLock<ClientContext>>,
|
||||||
own_username: String,
|
own_username: String,
|
||||||
) {
|
) {
|
||||||
thread::spawn(move || loop {
|
thread::spawn(move || {
|
||||||
match reader.recv() {
|
loop {
|
||||||
Ok(operation) => {
|
match reader.recv() {
|
||||||
match operation {
|
Ok(operation) => {
|
||||||
ClientOperation::ConnectToPeer(peer) => {
|
match operation {
|
||||||
let client_context_clone = client_context.clone();
|
ClientOperation::ConnectToPeer(peer) => {
|
||||||
let own_username_clone = own_username.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
Self::connect_to_peer(
|
|
||||||
peer,
|
|
||||||
client_context_clone,
|
|
||||||
own_username_clone,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ClientOperation::SearchResult(search_result) => {
|
|
||||||
trace!("[client] SearchResult {:?}", search_result);
|
|
||||||
let mut context = client_context.write().unwrap();
|
|
||||||
let result_token = search_result.token;
|
|
||||||
|
|
||||||
// Find the search with matching token
|
|
||||||
for search in context.searches.values_mut() {
|
|
||||||
if search.token == result_token {
|
|
||||||
search.results.push(search_result);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ClientOperation::PeerDisconnected(username, error) => {
|
|
||||||
let context = client_context.read().unwrap();
|
|
||||||
if let Some(ref registry) = context.peer_registry
|
|
||||||
&& let Some(handle) =
|
|
||||||
registry.remove_peer(&username)
|
|
||||||
{
|
|
||||||
let _ = handle.stop();
|
|
||||||
}
|
|
||||||
if let Some(error) = error {
|
|
||||||
warn!(
|
|
||||||
"[client] Peer {} disconnected with error: {:?}",
|
|
||||||
username, error
|
|
||||||
);
|
|
||||||
Self::process_failed_uploads(
|
|
||||||
client_context.clone(),
|
|
||||||
&username,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ClientOperation::PierceFireWall(peer) => {
|
|
||||||
Self::pierce_firewall(
|
|
||||||
peer,
|
|
||||||
client_context.clone(),
|
|
||||||
own_username.clone(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ClientOperation::DownloadFromPeer(
|
|
||||||
token,
|
|
||||||
peer,
|
|
||||||
allowed,
|
|
||||||
) => {
|
|
||||||
let maybe_download = {
|
|
||||||
let client_context =
|
|
||||||
client_context.read().unwrap();
|
|
||||||
client_context
|
|
||||||
.get_download_by_token(token)
|
|
||||||
.cloned()
|
|
||||||
};
|
|
||||||
let own_username = own_username.clone();
|
|
||||||
let client_context_clone = client_context.clone();
|
|
||||||
|
|
||||||
trace!(
|
|
||||||
"[client] DownloadFromPeer token: {} peer: {:?}",
|
|
||||||
token,
|
|
||||||
peer
|
|
||||||
);
|
|
||||||
match maybe_download {
|
|
||||||
Some(download) => {
|
|
||||||
thread::spawn(move || {
|
|
||||||
let download_peer = DownloadPeer::new(
|
|
||||||
download.username.clone(),
|
|
||||||
peer.host.clone(),
|
|
||||||
peer.port,
|
|
||||||
token,
|
|
||||||
allowed,
|
|
||||||
own_username,
|
|
||||||
);
|
|
||||||
let filename: Option<&str> = download
|
|
||||||
.filename
|
|
||||||
.split('\\')
|
|
||||||
.next_back();
|
|
||||||
match filename {
|
|
||||||
Some(filename) => {
|
|
||||||
match download_peer.download_file(
|
|
||||||
client_context_clone.clone(),
|
|
||||||
Some(download.clone()),
|
|
||||||
None
|
|
||||||
) {
|
|
||||||
Ok((download, filename)) => {
|
|
||||||
let _ = download.sender.send(DownloadStatus::Completed);
|
|
||||||
client_context_clone.write().unwrap().update_download_with_status(download.token, DownloadStatus::Completed);
|
|
||||||
info!("Successfully downloaded {} bytes to {}", download.size, filename);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let _ = download.sender.send(DownloadStatus::Failed);
|
|
||||||
client_context_clone.write().unwrap().update_download_with_status(download.token, DownloadStatus::Failed);
|
|
||||||
error!(
|
|
||||||
"Failed to download file '{}' from {}:{} (token: {}) - Error: {}",
|
|
||||||
filename, peer.host, peer.port, download.token, e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => error!("Cant find filename to save download: {:?}", download.filename),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
error!(
|
|
||||||
"Can't find download with token {:?}",
|
|
||||||
token
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
ClientOperation::NewPeer(new_peer) => {
|
|
||||||
let peer_exists = client_context
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.peer_registry
|
|
||||||
.as_ref()
|
|
||||||
.map(|r| r.contains(&new_peer.username))
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if peer_exists {
|
|
||||||
debug!(
|
|
||||||
"Already connected to {}",
|
|
||||||
new_peer.username
|
|
||||||
);
|
|
||||||
} else if let Some(server_sender) =
|
|
||||||
&client_context.read().unwrap().server_sender
|
|
||||||
{
|
|
||||||
server_sender
|
|
||||||
.send(ServerMessage::GetPeerAddress(
|
|
||||||
new_peer.username.clone(),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let addr = new_peer.tcp_stream.peer_addr().unwrap();
|
|
||||||
let host = addr.ip().to_string();
|
|
||||||
let port: u32 = addr.port().into();
|
|
||||||
|
|
||||||
let peer = Peer {
|
|
||||||
username: new_peer.username.clone(),
|
|
||||||
connection_type: new_peer.connection_type,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
token: Some(new_peer.token),
|
|
||||||
privileged: None,
|
|
||||||
obfuscated_port: None,
|
|
||||||
unknown: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Self::connect_to_peer(
|
|
||||||
peer,
|
|
||||||
client_context.clone(),
|
|
||||||
own_username.clone(),
|
|
||||||
Some(new_peer.tcp_stream),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ClientOperation::GetPeerAddressResponse {
|
|
||||||
username,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
obfuscation_type,
|
|
||||||
obfuscated_port,
|
|
||||||
} => {
|
|
||||||
debug!(
|
|
||||||
"Received peer address for {}: {}:{} (obf_type: {}, obf_port: {})",
|
|
||||||
username, host, port, obfuscation_type, obfuscated_port
|
|
||||||
);
|
|
||||||
|
|
||||||
let peer_exists = client_context
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.peer_registry
|
|
||||||
.as_ref()
|
|
||||||
.map(|r| r.contains(&username))
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if peer_exists {
|
|
||||||
// don't know if i should update? and or reconnect the peer
|
|
||||||
// debug!(
|
|
||||||
// "existing peer: {:?}, new peer details:
|
|
||||||
// username: {},
|
|
||||||
// host: {},
|
|
||||||
// port: {}
|
|
||||||
// obfuscation_type: {}
|
|
||||||
// obfuscated_port: {}",
|
|
||||||
// peer,
|
|
||||||
// username,
|
|
||||||
// host,
|
|
||||||
// port,
|
|
||||||
// obfuscation_type,
|
|
||||||
// obfuscated_port,
|
|
||||||
// );
|
|
||||||
} else {
|
|
||||||
let peer = Peer::new(
|
|
||||||
username,
|
|
||||||
ConnectionType::P,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
None,
|
|
||||||
0,
|
|
||||||
obfuscation_type.try_into().unwrap(),
|
|
||||||
obfuscated_port.try_into().unwrap(),
|
|
||||||
);
|
|
||||||
let client_context_clone =
|
let client_context_clone =
|
||||||
client_context.clone();
|
client_context.clone();
|
||||||
let own_username_clone = own_username.clone();
|
let own_username_clone = own_username.clone();
|
||||||
@@ -702,62 +491,319 @@ impl Client {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
ClientOperation::SearchResult(search_result) => {
|
||||||
ClientOperation::UpdateDownloadTokens(
|
|
||||||
transfer,
|
|
||||||
username,
|
|
||||||
) => {
|
|
||||||
let mut context = client_context.write().unwrap();
|
|
||||||
|
|
||||||
let download_to_update =
|
|
||||||
context.get_downloads().iter().find_map(|d| {
|
|
||||||
if d.username == username
|
|
||||||
&& d.filename == transfer.filename
|
|
||||||
{
|
|
||||||
Some((d.token, d.clone()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some((old_token, download)) =
|
|
||||||
download_to_update
|
|
||||||
{
|
|
||||||
trace!(
|
trace!(
|
||||||
"[client] UpdateDownloadTokens found {old_token}, transfer: {:?}",
|
"[client] SearchResult {:?}",
|
||||||
transfer
|
search_result
|
||||||
);
|
);
|
||||||
|
let mut context =
|
||||||
|
client_context.write().unwrap();
|
||||||
|
let result_token = search_result.token;
|
||||||
|
|
||||||
context.add_download(Download {
|
// Find the search with matching token
|
||||||
username: username.clone(),
|
for search in context.searches.values_mut() {
|
||||||
filename: transfer.filename,
|
if search.token == result_token {
|
||||||
token: transfer.token,
|
search.results.push(search_result);
|
||||||
size: transfer.size,
|
break;
|
||||||
download_directory: download
|
}
|
||||||
.download_directory,
|
}
|
||||||
status: download.status.clone(),
|
}
|
||||||
sender: download.sender.clone(),
|
ClientOperation::PeerDisconnected(
|
||||||
});
|
username,
|
||||||
context.remove_download(old_token);
|
error,
|
||||||
|
) => {
|
||||||
|
let context = client_context.read().unwrap();
|
||||||
|
if let Some(ref registry) =
|
||||||
|
context.peer_registry
|
||||||
|
&& let Some(handle) =
|
||||||
|
registry.remove_peer(&username)
|
||||||
|
{
|
||||||
|
let _ = handle.stop();
|
||||||
|
}
|
||||||
|
if let Some(error) = error {
|
||||||
|
warn!(
|
||||||
|
"[client] Peer {} disconnected with error: {:?}",
|
||||||
|
username, error
|
||||||
|
);
|
||||||
|
Self::process_failed_uploads(
|
||||||
|
client_context.clone(),
|
||||||
|
&username,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientOperation::PierceFireWall(peer) => {
|
||||||
|
Self::pierce_firewall(
|
||||||
|
peer,
|
||||||
|
client_context.clone(),
|
||||||
|
own_username.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ClientOperation::DownloadFromPeer(
|
||||||
|
token,
|
||||||
|
peer,
|
||||||
|
allowed,
|
||||||
|
) => {
|
||||||
|
let maybe_download = {
|
||||||
|
let client_context =
|
||||||
|
client_context.read().unwrap();
|
||||||
|
client_context
|
||||||
|
.get_download_by_token(token)
|
||||||
|
.cloned()
|
||||||
|
};
|
||||||
|
let own_username = own_username.clone();
|
||||||
|
let client_context_clone =
|
||||||
|
client_context.clone();
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"[client] DownloadFromPeer token: {} peer: {:?}",
|
||||||
|
token, peer
|
||||||
|
);
|
||||||
|
match maybe_download {
|
||||||
|
Some(download) => {
|
||||||
|
thread::spawn(move || {
|
||||||
|
let download_peer =
|
||||||
|
DownloadPeer::new(
|
||||||
|
download.username.clone(),
|
||||||
|
peer.host.clone(),
|
||||||
|
peer.port,
|
||||||
|
token,
|
||||||
|
allowed,
|
||||||
|
own_username,
|
||||||
|
);
|
||||||
|
let filename: Option<&str> =
|
||||||
|
download
|
||||||
|
.filename
|
||||||
|
.split('\\')
|
||||||
|
.next_back();
|
||||||
|
match filename {
|
||||||
|
Some(filename) => {
|
||||||
|
match download_peer
|
||||||
|
.download_file(
|
||||||
|
client_context_clone
|
||||||
|
.clone(),
|
||||||
|
Some(download.clone()),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok((
|
||||||
|
download,
|
||||||
|
filename,
|
||||||
|
)) => {
|
||||||
|
let _ = download.sender.send(DownloadStatus::Completed);
|
||||||
|
client_context_clone.write().unwrap().update_download_with_status(download.token, DownloadStatus::Completed);
|
||||||
|
info!(
|
||||||
|
"Successfully downloaded {} bytes to {}",
|
||||||
|
download.size,
|
||||||
|
filename
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let _ = download.sender.send(DownloadStatus::Failed);
|
||||||
|
client_context_clone.write().unwrap().update_download_with_status(download.token, DownloadStatus::Failed);
|
||||||
|
error!(
|
||||||
|
"Failed to download file '{}' from {}:{} (token: {}) - Error: {}",
|
||||||
|
filename,
|
||||||
|
peer.host,
|
||||||
|
peer.port,
|
||||||
|
download.token,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => error!(
|
||||||
|
"Cant find filename to save download: {:?}",
|
||||||
|
download.filename
|
||||||
|
),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
error!(
|
||||||
|
"Can't find download with token {:?}",
|
||||||
|
token
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ClientOperation::NewPeer(new_peer) => {
|
||||||
|
let peer_exists = client_context
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.peer_registry
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.contains(&new_peer.username))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if peer_exists {
|
||||||
|
debug!(
|
||||||
|
"Already connected to {}",
|
||||||
|
new_peer.username
|
||||||
|
);
|
||||||
|
} else if let Some(server_sender) =
|
||||||
|
&client_context
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.server_sender
|
||||||
|
{
|
||||||
|
server_sender
|
||||||
|
.send(ServerMessage::GetPeerAddress(
|
||||||
|
new_peer.username.clone(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let addr =
|
||||||
|
new_peer.tcp_stream.peer_addr().unwrap();
|
||||||
|
let host = addr.ip().to_string();
|
||||||
|
let port: u32 = addr.port().into();
|
||||||
|
|
||||||
|
let peer = Peer {
|
||||||
|
username: new_peer.username.clone(),
|
||||||
|
connection_type: new_peer.connection_type,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
token: Some(new_peer.token),
|
||||||
|
privileged: None,
|
||||||
|
obfuscated_port: None,
|
||||||
|
unknown: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::connect_to_peer(
|
||||||
|
peer,
|
||||||
|
client_context.clone(),
|
||||||
|
own_username.clone(),
|
||||||
|
Some(new_peer.tcp_stream),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ClientOperation::GetPeerAddressResponse {
|
||||||
|
username,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
obfuscation_type,
|
||||||
|
obfuscated_port,
|
||||||
|
} => {
|
||||||
|
debug!(
|
||||||
|
"Received peer address for {}: {}:{} (obf_type: {}, obf_port: {})",
|
||||||
|
username,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
obfuscation_type,
|
||||||
|
obfuscated_port
|
||||||
|
);
|
||||||
|
|
||||||
|
let peer_exists = client_context
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.peer_registry
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.contains(&username))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if peer_exists {
|
||||||
|
// don't know if i should update? and or reconnect the peer
|
||||||
|
// debug!(
|
||||||
|
// "existing peer: {:?}, new peer details:
|
||||||
|
// username: {},
|
||||||
|
// host: {},
|
||||||
|
// port: {}
|
||||||
|
// obfuscation_type: {}
|
||||||
|
// obfuscated_port: {}",
|
||||||
|
// peer,
|
||||||
|
// username,
|
||||||
|
// host,
|
||||||
|
// port,
|
||||||
|
// obfuscation_type,
|
||||||
|
// obfuscated_port,
|
||||||
|
// );
|
||||||
|
} else {
|
||||||
|
let peer = Peer::new(
|
||||||
|
username,
|
||||||
|
ConnectionType::P,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
obfuscation_type.try_into().unwrap(),
|
||||||
|
obfuscated_port.try_into().unwrap(),
|
||||||
|
);
|
||||||
|
let client_context_clone =
|
||||||
|
client_context.clone();
|
||||||
|
let own_username_clone =
|
||||||
|
own_username.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
Self::connect_to_peer(
|
||||||
|
peer,
|
||||||
|
client_context_clone,
|
||||||
|
own_username_clone,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientOperation::UpdateDownloadTokens(
|
||||||
|
transfer,
|
||||||
|
username,
|
||||||
|
) => {
|
||||||
|
let mut context =
|
||||||
|
client_context.write().unwrap();
|
||||||
|
|
||||||
|
let download_to_update = context
|
||||||
|
.get_downloads()
|
||||||
|
.iter()
|
||||||
|
.find_map(|d| {
|
||||||
|
if d.username == username
|
||||||
|
&& d.filename == transfer.filename
|
||||||
|
{
|
||||||
|
Some((d.token, d.clone()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some((old_token, download)) =
|
||||||
|
download_to_update
|
||||||
|
{
|
||||||
|
trace!(
|
||||||
|
"[client] UpdateDownloadTokens found {old_token}, transfer: {:?}",
|
||||||
|
transfer
|
||||||
|
);
|
||||||
|
|
||||||
|
context.add_download(Download {
|
||||||
|
username: username.clone(),
|
||||||
|
filename: transfer.filename,
|
||||||
|
token: transfer.token,
|
||||||
|
size: transfer.size,
|
||||||
|
download_directory: download
|
||||||
|
.download_directory,
|
||||||
|
status: download.status.clone(),
|
||||||
|
sender: download.sender.clone(),
|
||||||
|
});
|
||||||
|
context.remove_download(old_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientOperation::UploadFailed(
|
||||||
|
username,
|
||||||
|
filename,
|
||||||
|
) => {
|
||||||
|
Self::process_failed_uploads(
|
||||||
|
client_context.clone(),
|
||||||
|
&username,
|
||||||
|
Some(&filename),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ClientOperation::SetServerSender(sender) => {
|
||||||
|
client_context.write().unwrap().server_sender =
|
||||||
|
Some(sender);
|
||||||
|
debug!("[client] Server sender initialized");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClientOperation::UploadFailed(username, filename) => {
|
|
||||||
Self::process_failed_uploads(
|
|
||||||
client_context.clone(),
|
|
||||||
&username,
|
|
||||||
Some(&filename),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ClientOperation::SetServerSender(sender) => {
|
|
||||||
client_context.write().unwrap().server_sender =
|
|
||||||
Some(sender);
|
|
||||||
debug!("[client] Server sender initialized");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
Err(e) => {
|
||||||
Err(e) => {
|
error!("[client] Channel recv error: {:?}", e);
|
||||||
error!("[client] Channel recv error: {:?}", e);
|
break;
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -772,7 +818,10 @@ impl Client {
|
|||||||
let client_context = client_context.clone();
|
let client_context = client_context.clone();
|
||||||
|
|
||||||
let peer_clone = peer.clone();
|
let peer_clone = peer.clone();
|
||||||
trace!("[client] connecting to {}, with connection_type: {}, and token {:?}", peer.username, peer.connection_type, peer.token);
|
trace!(
|
||||||
|
"[client] connecting to {}, with connection_type: {}, and token {:?}",
|
||||||
|
peer.username, peer.connection_type, peer.token
|
||||||
|
);
|
||||||
match peer.connection_type {
|
match peer.connection_type {
|
||||||
ConnectionType::P => {
|
ConnectionType::P => {
|
||||||
let username = peer.username.clone();
|
let username = peer.username.clone();
|
||||||
@@ -784,8 +833,7 @@ impl Client {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
trace!(
|
trace!(
|
||||||
"Failed to spawn peer actor for {:?}: {:?}",
|
"Failed to spawn peer actor for {:?}: {:?}",
|
||||||
username,
|
username, e
|
||||||
e
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -797,8 +845,7 @@ impl Client {
|
|||||||
ConnectionType::F => {
|
ConnectionType::F => {
|
||||||
trace!(
|
trace!(
|
||||||
"[client] downloading from: {}, {:?}",
|
"[client] downloading from: {}, {:?}",
|
||||||
peer.username,
|
peer.username, peer.token
|
||||||
peer.token
|
|
||||||
);
|
);
|
||||||
let download_peer = DownloadPeer::new(
|
let download_peer = DownloadPeer::new(
|
||||||
peer.username,
|
peer.username,
|
||||||
@@ -817,8 +864,7 @@ impl Client {
|
|||||||
Ok((download, filename)) => {
|
Ok((download, filename)) => {
|
||||||
trace!(
|
trace!(
|
||||||
"[client] downloaded {} bytes {:?} ",
|
"[client] downloaded {} bytes {:?} ",
|
||||||
filename,
|
filename, download.size
|
||||||
download.size
|
|
||||||
);
|
);
|
||||||
let _ = download.sender.send(DownloadStatus::Completed);
|
let _ = download.sender.send(DownloadStatus::Completed);
|
||||||
client_context
|
client_context
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::message::{handlers::Handlers, Message};
|
use crate::message::{Message, handlers::Handlers};
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
|
|
||||||
use crate::warn;
|
use crate::warn;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
message::{server::MessageFactory, Message, MessageHandler},
|
message::{Message, MessageHandler, server::MessageFactory},
|
||||||
peer::PeerMessage,
|
peer::PeerMessage,
|
||||||
};
|
};
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ impl MessageHandler<PeerMessage> for PeerInit {
|
|||||||
let token = message.read_int32();
|
let token = message.read_int32();
|
||||||
trace!(
|
trace!(
|
||||||
"PeerInit: username: {}, connection_type: {}, token: {}",
|
"PeerInit: username: {}, connection_type: {}, token: {}",
|
||||||
username,
|
username, connection_type, token
|
||||||
connection_type,
|
|
||||||
token
|
|
||||||
);
|
);
|
||||||
|
|
||||||
sender.send(PeerMessage::SetUsername(username)).unwrap();
|
sender.send(PeerMessage::SetUsername(username)).unwrap();
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use crate::{debug, info};
|
|||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actor::server_actor::ServerMessage, message::handlers::MessageHandler,
|
actor::server_actor::ServerMessage, message::Message,
|
||||||
message::Message,
|
message::handlers::MessageHandler,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct FileSearchHandler;
|
pub struct FileSearchHandler;
|
||||||
|
|||||||
@@ -195,8 +195,7 @@ impl DownloadPeer {
|
|||||||
) -> Result<(), DownloadError> {
|
) -> Result<(), DownloadError> {
|
||||||
trace!(
|
trace!(
|
||||||
"[download_peer:{}] performing handshake no_pierce: {}",
|
"[download_peer:{}] performing handshake no_pierce: {}",
|
||||||
self.username,
|
self.username, self.no_pierce
|
||||||
self.no_pierce
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if !self.no_pierce {
|
if !self.no_pierce {
|
||||||
@@ -231,8 +230,7 @@ impl DownloadPeer {
|
|||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"[download_peer:{}] got token: {} from data chunk",
|
"[download_peer:{}] got token: {} from data chunk",
|
||||||
self.username,
|
self.username, token_u32
|
||||||
token_u32
|
|
||||||
);
|
);
|
||||||
|
|
||||||
stream
|
stream
|
||||||
@@ -277,8 +275,7 @@ impl DownloadPeer {
|
|||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
trace!(
|
trace!(
|
||||||
"[download_peer:{}] connection closed by peer. bytes read: {}",
|
"[download_peer:{}] connection closed by peer. bytes read: {}",
|
||||||
self.username,
|
self.username, processor.total_bytes
|
||||||
processor.total_bytes
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -294,9 +291,7 @@ impl DownloadPeer {
|
|||||||
)?;
|
)?;
|
||||||
trace!(
|
trace!(
|
||||||
"[download_peer:{}] got download info for token: {} - filename: {}",
|
"[download_peer:{}] got download info for token: {} - filename: {}",
|
||||||
self.username,
|
self.username, self.token, new_download.filename
|
||||||
self.token,
|
|
||||||
new_download.filename
|
|
||||||
);
|
);
|
||||||
download = Some(new_download);
|
download = Some(new_download);
|
||||||
processor.received = true;
|
processor.received = true;
|
||||||
@@ -307,30 +302,30 @@ impl DownloadPeer {
|
|||||||
chunk_counter += 1;
|
chunk_counter += 1;
|
||||||
|
|
||||||
if let Some(ref dl) = download
|
if let Some(ref dl) = download
|
||||||
&& chunk_counter % PROGRESS_UPDATE_CHUNKS == 0 {
|
&& chunk_counter % PROGRESS_UPDATE_CHUNKS == 0
|
||||||
let elapsed =
|
{
|
||||||
last_update_time.elapsed().as_secs_f64();
|
let elapsed = last_update_time.elapsed().as_secs_f64();
|
||||||
let bytes_since_last_update =
|
let bytes_since_last_update =
|
||||||
PROGRESS_UPDATE_CHUNKS * READ_BUFFER_SIZE;
|
PROGRESS_UPDATE_CHUNKS * READ_BUFFER_SIZE;
|
||||||
let speed = if elapsed > 0.0 {
|
let speed = if elapsed > 0.0 {
|
||||||
bytes_since_last_update as f64 / elapsed
|
bytes_since_last_update as f64 / elapsed
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let status = DownloadStatus::InProgress {
|
let status = DownloadStatus::InProgress {
|
||||||
bytes_downloaded: processor.total_bytes as u64,
|
bytes_downloaded: processor.total_bytes as u64,
|
||||||
total_bytes: dl.size,
|
total_bytes: dl.size,
|
||||||
speed_bytes_per_sec: speed,
|
speed_bytes_per_sec: speed,
|
||||||
};
|
};
|
||||||
let _ = dl.sender.send(status.clone());
|
let _ = dl.sender.send(status.clone());
|
||||||
client_context
|
client_context
|
||||||
.write()
|
.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.update_download_with_status(dl.token, status);
|
.update_download_with_status(dl.token, status);
|
||||||
|
|
||||||
last_update_time = Instant::now();
|
last_update_time = Instant::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
let expected_size = download
|
let expected_size = download
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -486,7 +481,9 @@ mod tests {
|
|||||||
"file.mp3"
|
"file.mp3"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
FileManager::extract_filename_from_path("@@bhfrv\\Soulseek Downloads\\complete\\Beatport Top Deep House (2021)\\michel test file.mp3"),
|
FileManager::extract_filename_from_path(
|
||||||
|
"@@bhfrv\\Soulseek Downloads\\complete\\Beatport Top Deep House (2021)\\michel test file.mp3"
|
||||||
|
),
|
||||||
"michel test file.mp3"
|
"michel test file.mp3"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::client::{ClientContext, ClientOperation};
|
|||||||
use crate::message::{Message, MessageReader};
|
use crate::message::{Message, MessageReader};
|
||||||
use crate::peer::{ConnectionType, DownloadPeer, Peer};
|
use crate::peer::{ConnectionType, DownloadPeer, Peer};
|
||||||
use crate::types::Download;
|
use crate::types::Download;
|
||||||
use crate::{debug, error, info, trace, DownloadStatus};
|
use crate::{DownloadStatus, debug, error, info, trace};
|
||||||
|
|
||||||
const PEER_INIT_MESSAGE_CODE: u8 = 1;
|
const PEER_INIT_MESSAGE_CODE: u8 = 1;
|
||||||
|
|
||||||
@@ -79,8 +79,7 @@ fn extract_download_from_buffer(
|
|||||||
let token = parse_token_from_buffer(&buffer, username)?;
|
let token = parse_token_from_buffer(&buffer, username)?;
|
||||||
trace!(
|
trace!(
|
||||||
"[listener:{}] got transfer_token: {} from data chunk",
|
"[listener:{}] got transfer_token: {} from data chunk",
|
||||||
username,
|
username, token
|
||||||
token
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let context = client_context.read().unwrap();
|
let context = client_context.read().unwrap();
|
||||||
@@ -88,7 +87,10 @@ fn extract_download_from_buffer(
|
|||||||
|
|
||||||
if download.is_none() {
|
if download.is_none() {
|
||||||
let download_tokens = context.get_download_tokens();
|
let download_tokens = context.get_download_tokens();
|
||||||
trace!("[listener:{peer_ip}:{peer_port}] download token not found: {:?}, download tokens: {:?}", token, download_tokens);
|
trace!(
|
||||||
|
"[listener:{peer_ip}:{peer_port}] download token not found: {:?}, download tokens: {:?}",
|
||||||
|
token, download_tokens
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
download
|
download
|
||||||
@@ -129,8 +131,7 @@ fn handle_file_connection(
|
|||||||
) {
|
) {
|
||||||
trace!(
|
trace!(
|
||||||
"[client] DownloadFromPeer token: {} peer: {:?}",
|
"[client] DownloadFromPeer token: {} peer: {:?}",
|
||||||
token,
|
token, peer
|
||||||
peer
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let download = extract_download_from_buffer(
|
let download = extract_download_from_buffer(
|
||||||
@@ -198,7 +199,9 @@ fn handle_incoming_connection(stream: TcpStream, context: ConnectionContext) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let Some(init_data) = parse_peer_init_message(message) else {
|
let Some(init_data) = parse_peer_init_message(message) else {
|
||||||
error!("[listener:{peer_ip}:{peer_port}] Invalid or unknown peer init message");
|
error!(
|
||||||
|
"[listener:{peer_ip}:{peer_port}] Invalid or unknown peer init message"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -241,7 +244,9 @@ fn handle_incoming_connection(stream: TcpStream, context: ConnectionContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
ConnectionType::D => {
|
ConnectionType::D => {
|
||||||
debug!("[listener:{peer_ip}:{peer_port}] connection type is D, not supported yet, closing connection. ");
|
debug!(
|
||||||
|
"[listener:{peer_ip}:{peer_port}] connection type is D, not supported yet, closing connection. "
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use std::env;
|
|||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Mutex, Once,
|
Mutex, Once,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
@@ -168,10 +168,11 @@ pub fn log(level: LogLevel, message: &str) {
|
|||||||
}
|
}
|
||||||
// But ALSO write to file if configured (file logging bypasses buffering)
|
// But ALSO write to file if configured (file logging bypasses buffering)
|
||||||
if let Ok(mut log_file) = LOG_FILE.lock()
|
if let Ok(mut log_file) = LOG_FILE.lock()
|
||||||
&& let Some(file) = log_file.as_mut() {
|
&& let Some(file) = log_file.as_mut()
|
||||||
let _ = writeln!(file, "{}", formatted_message_plain);
|
{
|
||||||
let _ = file.flush();
|
let _ = writeln!(file, "{}", formatted_message_plain);
|
||||||
}
|
let _ = file.flush();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Write to file if configured, otherwise to stderr
|
// Write to file if configured, otherwise to stderr
|
||||||
if let Ok(mut log_file) = LOG_FILE.lock() {
|
if let Ok(mut log_file) = LOG_FILE.lock() {
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ fn bit_padding(input: &str) -> Vec<u8> {
|
|||||||
//check if bit length % 512 is 448 (64 less than 512)
|
//check if bit length % 512 is 448 (64 less than 512)
|
||||||
while (input_vector.len() * 8) % 512 != 448 {
|
while (input_vector.len() * 8) % 512 != 448 {
|
||||||
input_vector.push(0_u8); // push in another 8-bit 0 padded value until the correct
|
input_vector.push(0_u8); // push in another 8-bit 0 padded value until the correct
|
||||||
// result is reached;
|
// result is reached;
|
||||||
}
|
}
|
||||||
|
|
||||||
let length_bits_as_u8_array = split_u64_to_u8_array(bit_length);
|
let length_bits_as_u8_array = split_u64_to_u8_array(bit_length);
|
||||||
@@ -467,8 +467,7 @@ fn all_alphanumeric_correct_hash() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repeated_numbers_correct_hash() {
|
fn repeated_numbers_correct_hash() {
|
||||||
let repeated_number_str =
|
let repeated_number_str = "12345678901234567890123456789012345678901234567890123456789012345678901234567890";
|
||||||
"12345678901234567890123456789012345678901234567890123456789012345678901234567890";
|
|
||||||
|
|
||||||
assert_eq!("57edf4a22be3c955ac49da2e2107b67a", md5(repeated_number_str));
|
assert_eq!("57edf4a22be3c955ac49da2e2107b67a", md5(repeated_number_str));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::{mpsc, Arc, Mutex};
|
use std::sync::{Arc, Mutex, mpsc};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
type Job = Box<dyn FnOnce() + Send + 'static>;
|
type Job = Box<dyn FnOnce() + Send + 'static>;
|
||||||
@@ -89,37 +89,39 @@ impl Worker {
|
|||||||
_active_threads: Arc<AtomicUsize>,
|
_active_threads: Arc<AtomicUsize>,
|
||||||
_total_threads: usize,
|
_total_threads: usize,
|
||||||
) -> Worker {
|
) -> Worker {
|
||||||
let thread = thread::spawn(move || loop {
|
let thread = thread::spawn(move || {
|
||||||
let message = receiver.lock().unwrap().recv();
|
loop {
|
||||||
match message {
|
let message = receiver.lock().unwrap().recv();
|
||||||
Ok(Message::NewJob(job)) => {
|
match message {
|
||||||
// let active =
|
Ok(Message::NewJob(job)) => {
|
||||||
// active_threads.fetch_add(1, Ordering::SeqCst) + 1;
|
// let active =
|
||||||
// trace!(
|
// active_threads.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
// "Thread {} started job (active: {}/{})",
|
// trace!(
|
||||||
// id,
|
// "Thread {} started job (active: {}/{})",
|
||||||
// active,
|
// id,
|
||||||
// total_threads
|
// active,
|
||||||
// );
|
// total_threads
|
||||||
|
// );
|
||||||
|
|
||||||
job();
|
job();
|
||||||
|
|
||||||
// let active =
|
// let active =
|
||||||
// active_threads.fetch_sub(1, Ordering::SeqCst) - 1;
|
// active_threads.fetch_sub(1, Ordering::SeqCst) - 1;
|
||||||
// trace!(
|
// trace!(
|
||||||
// "Thread {} finished job (active: {}/{})",
|
// "Thread {} finished job (active: {}/{})",
|
||||||
// id,
|
// id,
|
||||||
// active,
|
// active,
|
||||||
// total_threads
|
// total_threads
|
||||||
// );
|
// );
|
||||||
}
|
}
|
||||||
Ok(Message::Terminate) => {
|
Ok(Message::Terminate) => {
|
||||||
// Received termination signal
|
// Received termination signal
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Channel disconnected
|
// Channel disconnected
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+16
-8
@@ -4,16 +4,16 @@ mod models;
|
|||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{parse_server_address, Cli, Commands};
|
use cli::{Cli, Commands, parse_server_address};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use config::SearchConfig;
|
use config::SearchConfig;
|
||||||
use soulseek_rs::{Client, ClientSettings, PeerAddress};
|
use soulseek_rs::{Client, ClientSettings, PeerAddress};
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{Arc, atomic::AtomicBool},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use ui::{launch_main_tui, show_multi_download_progress, FileSelector};
|
use ui::{FileSelector, launch_main_tui, show_multi_download_progress};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
@@ -33,14 +33,22 @@ fn main() -> Result<()> {
|
|||||||
// Set LOG_FILE env var if provided via CLI
|
// Set LOG_FILE env var if provided via CLI
|
||||||
if let Some(log_file) = &cli.log_file {
|
if let Some(log_file) = &cli.log_file {
|
||||||
// SAFETY: Called before any threads are spawned
|
// SAFETY: Called before any threads are spawned
|
||||||
unsafe { env::set_var("LOG_FILE", log_file.to_string_lossy().to_string()) };
|
unsafe {
|
||||||
|
env::set_var("LOG_FILE", log_file.to_string_lossy().to_string())
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let username = cli.username
|
let username = cli.username.ok_or_else(|| {
|
||||||
.ok_or_else(|| color_eyre::eyre::eyre!("Username required: use --username or set SOULSEEK_USERNAME env var"))?;
|
color_eyre::eyre::eyre!(
|
||||||
|
"Username required: use --username or set SOULSEEK_USERNAME env var"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let password = cli.password
|
let password = cli.password.ok_or_else(|| {
|
||||||
.ok_or_else(|| color_eyre::eyre::eyre!("Password required: use --password or set SOULSEEK_PASSWORD env var"))?;
|
color_eyre::eyre::eyre!(
|
||||||
|
"Password required: use --password or set SOULSEEK_PASSWORD env var"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let (server_host, server_port) = parse_server_address(&cli.server)?;
|
let (server_host, server_port) = parse_server_address(&cli.server)?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::models::FileDisplayData;
|
use crate::models::FileDisplayData;
|
||||||
use ratatui::{layout::Rect, widgets::TableState};
|
use ratatui::{layout::Rect, widgets::TableState};
|
||||||
use soulseek_rs::{types::Download, DownloadStatus};
|
use soulseek_rs::{DownloadStatus, types::Download};
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::{mpsc::Receiver, mpsc::Sender, Arc};
|
use std::sync::{Arc, mpsc::Receiver, mpsc::Sender};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
use crate::models::FileDisplayData;
|
use crate::models::FileDisplayData;
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
border_style, border_type, format_bytes, format_shortcuts_styled,
|
HIGHLIGHT_SYMBOL, border_style, border_type, format_bytes,
|
||||||
get_bitrate, get_spinner_char, header_style, highlight_style,
|
format_shortcuts_styled, get_bitrate, get_spinner_char, header_style,
|
||||||
primary_style, success_style, warning_style, HIGHLIGHT_SYMBOL,
|
highlight_style, primary_style, success_style, warning_style,
|
||||||
};
|
};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{self, poll, Event, KeyCode, KeyEvent, KeyEventKind},
|
DefaultTerminal, Frame,
|
||||||
|
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, poll},
|
||||||
layout::{Alignment, Constraint, Layout},
|
layout::{Alignment, Constraint, Layout},
|
||||||
style::Style,
|
style::Style,
|
||||||
widgets::{
|
widgets::{
|
||||||
Block, Borders, Cell, HighlightSpacing, Paragraph, Row, StatefulWidget,
|
Block, Borders, Cell, HighlightSpacing, Paragraph, Row, StatefulWidget,
|
||||||
Table, TableState, Wrap,
|
Table, TableState, Wrap,
|
||||||
},
|
},
|
||||||
DefaultTerminal, Frame,
|
|
||||||
};
|
};
|
||||||
use soulseek_rs::Client;
|
use soulseek_rs::Client;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
},
|
},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
@@ -114,9 +114,10 @@ impl FileSelector {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if poll(timeout)?
|
if poll(timeout)?
|
||||||
&& let Event::Key(key) = event::read()? {
|
&& let Event::Key(key) = event::read()?
|
||||||
self.handle_key(key);
|
{
|
||||||
}
|
self.handle_key(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((terminal, self.selected_indices.iter().copied().collect()))
|
Ok((terminal, self.selected_indices.iter().copied().collect()))
|
||||||
@@ -158,13 +159,13 @@ impl FileSelector {
|
|||||||
fn toggle_selection(&mut self) {
|
fn toggle_selection(&mut self) {
|
||||||
if let Some(filtered_idx) = self.state.selected()
|
if let Some(filtered_idx) = self.state.selected()
|
||||||
&& let Some(&original_idx) = self.filtered_indices.get(filtered_idx)
|
&& let Some(&original_idx) = self.filtered_indices.get(filtered_idx)
|
||||||
{
|
{
|
||||||
if self.selected_indices.contains(&original_idx) {
|
if self.selected_indices.contains(&original_idx) {
|
||||||
self.selected_indices.remove(&original_idx);
|
self.selected_indices.remove(&original_idx);
|
||||||
} else {
|
} else {
|
||||||
self.selected_indices.insert(original_idx);
|
self.selected_indices.insert(original_idx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_key(&mut self, key: KeyEvent) {
|
fn handle_key(&mut self, key: KeyEvent) {
|
||||||
@@ -194,11 +195,11 @@ impl FileSelector {
|
|||||||
// If nothing selected, select the current item under cursor
|
// If nothing selected, select the current item under cursor
|
||||||
if self.selected_indices.is_empty()
|
if self.selected_indices.is_empty()
|
||||||
&& let Some(filtered_idx) = self.state.selected()
|
&& let Some(filtered_idx) = self.state.selected()
|
||||||
&& let Some(&original_idx) =
|
&& let Some(&original_idx) =
|
||||||
self.filtered_indices.get(filtered_idx)
|
self.filtered_indices.get(filtered_idx)
|
||||||
{
|
{
|
||||||
self.selected_indices.insert(original_idx);
|
self.selected_indices.insert(original_idx);
|
||||||
}
|
}
|
||||||
self.should_exit = true;
|
self.should_exit = true;
|
||||||
}
|
}
|
||||||
KeyCode::Up | KeyCode::Down => {
|
KeyCode::Up | KeyCode::Down => {
|
||||||
@@ -236,11 +237,11 @@ impl FileSelector {
|
|||||||
// If nothing selected, select the current item under cursor
|
// If nothing selected, select the current item under cursor
|
||||||
if self.selected_indices.is_empty()
|
if self.selected_indices.is_empty()
|
||||||
&& let Some(filtered_idx) = self.state.selected()
|
&& let Some(filtered_idx) = self.state.selected()
|
||||||
&& let Some(&original_idx) =
|
&& let Some(&original_idx) =
|
||||||
self.filtered_indices.get(filtered_idx)
|
self.filtered_indices.get(filtered_idx)
|
||||||
{
|
{
|
||||||
self.selected_indices.insert(original_idx);
|
self.selected_indices.insert(original_idx);
|
||||||
}
|
}
|
||||||
self.should_exit = true;
|
self.should_exit = true;
|
||||||
}
|
}
|
||||||
KeyCode::Esc | KeyCode::Char('q') => {
|
KeyCode::Esc | KeyCode::Char('q') => {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
use crate::models::DownloadEntry;
|
use crate::models::DownloadEntry;
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
border_style, border_type, error_style, format_bytes_progress,
|
COLOR_PRIMARY, HIGHLIGHT_SYMBOL, border_style, border_type, error_style,
|
||||||
format_progress_bar, format_shortcuts_styled, format_speed, header_style,
|
format_bytes_progress, format_progress_bar, format_shortcuts_styled,
|
||||||
highlight_style, inactive_style, primary_style, warning_style,
|
format_speed, header_style, highlight_style, inactive_style, primary_style,
|
||||||
COLOR_PRIMARY, HIGHLIGHT_SYMBOL,
|
warning_style,
|
||||||
};
|
};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{self, poll, Event, KeyCode, KeyEvent, KeyEventKind},
|
DefaultTerminal, Frame,
|
||||||
|
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, poll},
|
||||||
layout::{Alignment, Constraint, Layout},
|
layout::{Alignment, Constraint, Layout},
|
||||||
style::{Modifier, Style},
|
style::{Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
@@ -15,11 +16,10 @@ use ratatui::{
|
|||||||
Block, Borders, Cell, HighlightSpacing, Paragraph, Row, StatefulWidget,
|
Block, Borders, Cell, HighlightSpacing, Paragraph, Row, StatefulWidget,
|
||||||
Table, TableState,
|
Table, TableState,
|
||||||
},
|
},
|
||||||
DefaultTerminal, Frame,
|
|
||||||
};
|
};
|
||||||
use soulseek_rs::{Client, DownloadStatus};
|
use soulseek_rs::{Client, DownloadStatus};
|
||||||
use std::{
|
use std::{
|
||||||
sync::{mpsc, mpsc::Receiver, Arc},
|
sync::{Arc, mpsc, mpsc::Receiver},
|
||||||
thread,
|
thread,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@@ -82,18 +82,18 @@ impl MultiDownloadProgress {
|
|||||||
let mut need_start_next = false;
|
let mut need_start_next = false;
|
||||||
for download_entry in &mut self.downloads {
|
for download_entry in &mut self.downloads {
|
||||||
if let Some(receiver) = &download_entry.receiver
|
if let Some(receiver) = &download_entry.receiver
|
||||||
&& let Ok(status) = receiver.try_recv() {
|
&& let Ok(status) = receiver.try_recv()
|
||||||
let was_active = !download_entry.download.is_finished();
|
{
|
||||||
download_entry.download.status = status;
|
let was_active = !download_entry.download.is_finished();
|
||||||
let is_finished = download_entry.download.is_finished();
|
download_entry.download.status = status;
|
||||||
|
let is_finished = download_entry.download.is_finished();
|
||||||
|
|
||||||
// If download just finished, decrement active count
|
// If download just finished, decrement active count
|
||||||
if was_active && is_finished {
|
if was_active && is_finished {
|
||||||
self.active_count =
|
self.active_count = self.active_count.saturating_sub(1);
|
||||||
self.active_count.saturating_sub(1);
|
need_start_next = true;
|
||||||
need_start_next = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start next batch after iteration completes
|
// Start next batch after iteration completes
|
||||||
@@ -103,9 +103,10 @@ impl MultiDownloadProgress {
|
|||||||
|
|
||||||
// Handle keyboard input
|
// Handle keyboard input
|
||||||
if poll(Duration::from_millis(100))?
|
if poll(Duration::from_millis(100))?
|
||||||
&& let Event::Key(key) = event::read()? {
|
&& let Event::Key(key) = event::read()?
|
||||||
self.handle_key(key);
|
{
|
||||||
}
|
self.handle_key(key);
|
||||||
|
}
|
||||||
|
|
||||||
// Exit when user cancels or all downloads finished
|
// Exit when user cancels or all downloads finished
|
||||||
if self.should_exit || self.all_finished() {
|
if self.should_exit || self.all_finished() {
|
||||||
|
|||||||
@@ -3,28 +3,28 @@ use crate::models::{
|
|||||||
SearchStatus,
|
SearchStatus,
|
||||||
};
|
};
|
||||||
use crate::ui::panes::{
|
use crate::ui::panes::{
|
||||||
render_downloads_pane, render_results_pane, render_searches_pane,
|
ResultsPaneParams, render_downloads_pane, render_results_pane,
|
||||||
ResultsPaneParams,
|
render_searches_pane,
|
||||||
};
|
};
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
border_style, border_type, format_shortcuts_styled, render_download_stats,
|
border_style, border_type, format_shortcuts_styled, render_download_stats,
|
||||||
};
|
};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
DefaultTerminal, Frame,
|
||||||
crossterm::event::{
|
crossterm::event::{
|
||||||
self, poll, Event, KeyCode, KeyEvent, KeyEventKind, MouseButton,
|
self, Event, KeyCode, KeyEvent, KeyEventKind, MouseButton, MouseEvent,
|
||||||
MouseEvent, MouseEventKind,
|
MouseEventKind, poll,
|
||||||
},
|
},
|
||||||
layout::{Constraint, Layout},
|
layout::{Constraint, Layout},
|
||||||
widgets::{Block, Borders, Paragraph},
|
widgets::{Block, Borders, Paragraph},
|
||||||
DefaultTerminal, Frame,
|
|
||||||
};
|
};
|
||||||
use soulseek_rs::{Client, DownloadStatus};
|
use soulseek_rs::{Client, DownloadStatus};
|
||||||
use std::{
|
use std::{
|
||||||
sync::{
|
sync::{
|
||||||
|
Arc,
|
||||||
atomic::AtomicBool,
|
atomic::AtomicBool,
|
||||||
mpsc::{self},
|
mpsc::{self},
|
||||||
Arc,
|
|
||||||
},
|
},
|
||||||
thread,
|
thread,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
@@ -521,35 +521,35 @@ impl MainTui {
|
|||||||
// Check if click is within searches pane
|
// Check if click is within searches pane
|
||||||
if let Some(area) = self.state.searches_pane_area
|
if let Some(area) = self.state.searches_pane_area
|
||||||
&& col >= area.x
|
&& col >= area.x
|
||||||
&& col < area.x + area.width
|
&& col < area.x + area.width
|
||||||
&& row >= area.y
|
&& row >= area.y
|
||||||
&& row < area.y + area.height
|
&& row < area.y + area.height
|
||||||
{
|
{
|
||||||
self.state.focused_pane = FocusedPane::Searches;
|
self.state.focused_pane = FocusedPane::Searches;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if click is within results pane
|
// Check if click is within results pane
|
||||||
if let Some(area) = self.state.results_pane_area
|
if let Some(area) = self.state.results_pane_area
|
||||||
&& col >= area.x
|
&& col >= area.x
|
||||||
&& col < area.x + area.width
|
&& col < area.x + area.width
|
||||||
&& row >= area.y
|
&& row >= area.y
|
||||||
&& row < area.y + area.height
|
&& row < area.y + area.height
|
||||||
{
|
{
|
||||||
self.state.focused_pane = FocusedPane::Results;
|
self.state.focused_pane = FocusedPane::Results;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if click is within downloads pane
|
// Check if click is within downloads pane
|
||||||
if let Some(area) = self.state.downloads_pane_area
|
if let Some(area) = self.state.downloads_pane_area
|
||||||
&& col >= area.x
|
&& col >= area.x
|
||||||
&& col < area.x + area.width
|
&& col < area.x + area.width
|
||||||
&& row >= area.y
|
&& row >= area.y
|
||||||
&& row < area.y + area.height
|
&& row < area.y + area.height
|
||||||
{
|
{
|
||||||
self.state.focused_pane = FocusedPane::Downloads;
|
self.state.focused_pane = FocusedPane::Downloads;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -747,13 +747,14 @@ impl MainTui {
|
|||||||
|
|
||||||
// Update selected search if this is the active one
|
// Update selected search if this is the active one
|
||||||
if let Some(selected_idx) = selected_search_index
|
if let Some(selected_idx) = selected_search_index
|
||||||
&& selected_idx == idx {
|
&& selected_idx == idx
|
||||||
self.state.results_items = search.results.clone();
|
{
|
||||||
self.state.results_filtered_items =
|
self.state.results_items = search.results.clone();
|
||||||
search.results.clone();
|
self.state.results_filtered_items =
|
||||||
self.state.results_filtered_indices =
|
search.results.clone();
|
||||||
(0..search.results.len()).collect();
|
self.state.results_filtered_indices =
|
||||||
}
|
(0..search.results.len()).collect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as completed after timeout
|
// Mark as completed after timeout
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
use crate::models::DownloadEntry;
|
use crate::models::DownloadEntry;
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
border_style, border_type, error_style, format_bytes, format_speed,
|
HIGHLIGHT_SYMBOL, border_style, border_type, error_style, format_bytes,
|
||||||
header_style, highlight_style, inactive_style, success_style,
|
format_speed, header_style, highlight_style, inactive_style, success_style,
|
||||||
warning_style, HIGHLIGHT_SYMBOL,
|
warning_style,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
widgets::{
|
widgets::{
|
||||||
Block, Borders, Cell, HighlightSpacing, Paragraph, Row, Table,
|
Block, Borders, Cell, HighlightSpacing, Paragraph, Row, Table,
|
||||||
TableState,
|
TableState,
|
||||||
},
|
},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
use soulseek_rs::DownloadStatus;
|
use soulseek_rs::DownloadStatus;
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ mod results_pane;
|
|||||||
mod searches_pane;
|
mod searches_pane;
|
||||||
|
|
||||||
pub use downloads_pane::render_downloads_pane;
|
pub use downloads_pane::render_downloads_pane;
|
||||||
pub use results_pane::{render_results_pane, ResultsPaneParams};
|
pub use results_pane::{ResultsPaneParams, render_results_pane};
|
||||||
pub use searches_pane::render_searches_pane;
|
pub use searches_pane::render_searches_pane;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use crate::models::FileDisplayData;
|
use crate::models::FileDisplayData;
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
border_style, border_type, format_bytes, header_style, highlight_style,
|
HIGHLIGHT_SYMBOL, border_style, border_type, format_bytes, header_style,
|
||||||
HIGHLIGHT_SYMBOL,
|
highlight_style,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
widgets::{
|
widgets::{
|
||||||
Block, Borders, Cell, HighlightSpacing, Paragraph, Row, Table,
|
Block, Borders, Cell, HighlightSpacing, Paragraph, Row, Table,
|
||||||
TableState,
|
TableState,
|
||||||
},
|
},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use crate::models::{SearchEntry, SearchStatus};
|
use crate::models::{SearchEntry, SearchStatus};
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
border_style, border_type, header_style, highlight_style, success_style,
|
HIGHLIGHT_SYMBOL, border_style, border_type, header_style, highlight_style,
|
||||||
warning_style, HIGHLIGHT_SYMBOL,
|
success_style, warning_style,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
widgets::{Block, Borders, Cell, HighlightSpacing, Row, Table, TableState},
|
widgets::{Block, Borders, Cell, HighlightSpacing, Row, Table, TableState},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn render_searches_pane(
|
pub fn render_searches_pane(
|
||||||
|
|||||||
Reference in New Issue
Block a user