From dc2febf8e9207c64ae4ca2e07f9f5834ada8a633 Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Thu, 30 Oct 2025 13:55:46 -0700 Subject: [PATCH 01/16] feat: Add a `Torrent::update_trackers` function for updating tracker state --- crates/libtortillas/src/torrent/actor.rs | 28 +++++++++++++++++++++++- crates/libtortillas/src/tracker/mod.rs | 1 + 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/libtortillas/src/torrent/actor.rs b/crates/libtortillas/src/torrent/actor.rs index d82a328..713c0b9 100644 --- a/crates/libtortillas/src/torrent/actor.rs +++ b/crates/libtortillas/src/torrent/actor.rs @@ -31,7 +31,7 @@ use crate::{ TorrentExport, piece_manager::{FilePieceManager, PieceManager}, }, - tracker::{Tracker, TrackerActor, udp::UdpServer}, + tracker::{Tracker, TrackerActor, TrackerUpdate, udp::UdpServer}, }; pub const BLOCK_SIZE: usize = 16 * 1024; @@ -463,6 +463,32 @@ impl TorrentActor { } // Returns immediately, without waiting for any peer responses } + + #[instrument(skip(self, message), fields(torrent_id = %self.info_hash()))] + pub(super) async fn update_trackers(&self, message: TrackerUpdate) { + let trackers = self.trackers.clone(); + + let actor_refs: Vec<(Tracker, ActorRef)> = trackers + .iter() + .map(|entry| (entry.key().clone(), entry.value().clone())) + .collect(); + + for (uri, actor) in actor_refs { + let msg = message.clone(); + let trackers = trackers.clone(); + + tokio::spawn(async move { + if actor.is_alive() { + if let Err(e) = actor.tell(msg).await { + warn!(error = %e, tracker_uri = ?uri, "Failed to send to tracker"); + } + } else { + trace!(tracker_uri = ?uri, "Tracker actor is dead, removing from trackers set"); + trackers.remove(&uri); + } + }); + } + } /// Gets the path to a piece file based on the index. Only should be used /// when the piece storage strategy is [`Disk`](PieceStorageStrategy::Disk), /// this function will panic otherwise. diff --git a/crates/libtortillas/src/tracker/mod.rs b/crates/libtortillas/src/tracker/mod.rs index 275e345..6d62986 100644 --- a/crates/libtortillas/src/tracker/mod.rs +++ b/crates/libtortillas/src/tracker/mod.rs @@ -347,6 +347,7 @@ impl Message for TrackerActor { } /// Updates the tracker's announce fields +#[derive(Debug, Clone)] pub enum TrackerUpdate { /// The amount of data uploaded, in bytes Uploaded(usize), From 1c142cd4e133ea2ebc400621ee67aa442572e27d Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Thu, 30 Oct 2025 14:41:06 -0700 Subject: [PATCH 02/16] refactor: Use `Empty` as default state for trackers --- crates/libtortillas/src/tracker/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/libtortillas/src/tracker/mod.rs b/crates/libtortillas/src/tracker/mod.rs index 6d62986..e2e3eef 100644 --- a/crates/libtortillas/src/tracker/mod.rs +++ b/crates/libtortillas/src/tracker/mod.rs @@ -384,8 +384,8 @@ impl Message for TrackerActor { )] #[repr(u32)] pub enum Event { - Empty = 0, #[default] + Empty = 0, Started = 1, Completed = 2, Stopped = 3, From 1d8b2b193ada695540211cdd8ee8f3b2fad87c5b Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Thu, 30 Oct 2025 14:41:20 -0700 Subject: [PATCH 03/16] refactor: Don't include tracker event if event is `Empty` --- crates/libtortillas/src/tracker/http.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/libtortillas/src/tracker/http.rs b/crates/libtortillas/src/tracker/http.rs index 84b285f..fb99779 100644 --- a/crates/libtortillas/src/tracker/http.rs +++ b/crates/libtortillas/src/tracker/http.rs @@ -80,9 +80,12 @@ impl TrackerRequest { if let Some(left) = self.left { params.push(format!("left={left}")); } - let event_str = format!("{:?}", self.event).to_lowercase(); // Hack to get the string representation of the enum - params.push(format!("event={event_str}")); + // Don't include event if it's empty + if self.event != Event::Empty { + params.push(format!("event={:?}", self.event).to_lowercase()); + } + params.push(format!("compact={}", self.compact.unwrap_or(true) as u8)); params.join("&") From d654ce7e078d35e753477d7306da832f003e089c Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Thu, 30 Oct 2025 16:45:15 -0700 Subject: [PATCH 04/16] feat: Update tracker state based on bitfield size in `TorrentActor::start` --- crates/libtortillas/src/torrent/actor.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/libtortillas/src/torrent/actor.rs b/crates/libtortillas/src/torrent/actor.rs index 713c0b9..b5eec61 100644 --- a/crates/libtortillas/src/torrent/actor.rs +++ b/crates/libtortillas/src/torrent/actor.rs @@ -31,7 +31,7 @@ use crate::{ TorrentExport, piece_manager::{FilePieceManager, PieceManager}, }, - tracker::{Tracker, TrackerActor, TrackerUpdate, udp::UdpServer}, + tracker::{Event, Tracker, TrackerActor, TrackerUpdate, udp::UdpServer}, }; pub const BLOCK_SIZE: usize = 16 * 1024; @@ -257,6 +257,9 @@ impl TorrentActor { if self.is_full() { self.state = TorrentState::Seeding; info!(id = %self.info_hash(), "Torrent is now seeding"); + self + .update_trackers(TrackerUpdate::Event(Event::Completed)) + .await; } else { self.state = TorrentState::Downloading; info!(id = %self.info_hash(), "Torrent is now downloading"); @@ -264,19 +267,28 @@ impl TorrentActor { trace!(id = %self.info_hash(), peer_count = self.peers.len(), "Requesting first piece from peers"); self.next_piece = self.bitfield.first_zero().unwrap_or_default(); + // Announce that we have started + self + .update_trackers(TrackerUpdate::Event(Event::Started)) + .await; + // Request first piece from peers self .broadcast_to_peers(PeerTell::NeedPiece(self.next_piece, 0, BLOCK_SIZE)) .await; self.start_time = Some(Instant::now()); } + // Send ready hook if let Some(err) = self.ready_hook.take().and_then(|hook| hook.send(()).err()) { error!(?err, "Failed to send ready hook"); } + let Some(info) = self.info.as_ref() else { warn!(id = %self.info_hash(), "Start requested before info dict is available; deferring"); return; }; + + // Start piece manager self .piece_manager // Probably not the best to clone here, but should be fine for now From 4a7d66593baa83eb61b9c3481b568dab7fbbdc5f Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Fri, 31 Oct 2025 11:24:29 -0700 Subject: [PATCH 05/16] docs: Add documentation for `TorrentActor::update_trackers` --- crates/libtortillas/src/torrent/actor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/libtortillas/src/torrent/actor.rs b/crates/libtortillas/src/torrent/actor.rs index b5eec61..cfd9d1d 100644 --- a/crates/libtortillas/src/torrent/actor.rs +++ b/crates/libtortillas/src/torrent/actor.rs @@ -476,6 +476,8 @@ impl TorrentActor { // Returns immediately, without waiting for any peer responses } + /// Broadcasts a [`TrackerUpdate`] to all trackers concurrently. similar to + /// [`Self::broadcast_to_peers`], but for trackers. #[instrument(skip(self, message), fields(torrent_id = %self.info_hash()))] pub(super) async fn update_trackers(&self, message: TrackerUpdate) { let trackers = self.trackers.clone(); From 3de58719ce9f5897bb73d6f43cec746c4c77632e Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Fri, 31 Oct 2025 11:27:02 -0700 Subject: [PATCH 06/16] feat: Add `TorrentActor::broadcast_to_trackers` --- crates/libtortillas/src/torrent/actor.rs | 24 ++++++++++++++++++++++++ crates/libtortillas/src/tracker/mod.rs | 1 + 2 files changed, 25 insertions(+) diff --git a/crates/libtortillas/src/torrent/actor.rs b/crates/libtortillas/src/torrent/actor.rs index cfd9d1d..c21c82a 100644 --- a/crates/libtortillas/src/torrent/actor.rs +++ b/crates/libtortillas/src/torrent/actor.rs @@ -503,6 +503,30 @@ impl TorrentActor { }); } } + pub(super) async fn broadcast_to_trackers(&self, message: TrackerMessage) { + let trackers = self.trackers.clone(); + + let actor_refs: Vec<(Tracker, ActorRef)> = trackers + .iter() + .map(|entry| (entry.key().clone(), entry.value().clone())) + .collect(); + + for (uri, actor) in actor_refs { + let trackers = trackers.clone(); + + tokio::spawn(async move { + if actor.is_alive() { + if let Err(e) = actor.tell(message).await { + warn!(error = %e, tracker_uri = ?uri, "Failed to send to tracker"); + } + } else { + trace!(tracker_uri = ?uri, "Tracker actor is dead, removing from trackers set"); + trackers.remove(&uri); + } + }); + } + } + /// Gets the path to a piece file based on the index. Only should be used /// when the piece storage strategy is [`Disk`](PieceStorageStrategy::Disk), /// this function will panic otherwise. diff --git a/crates/libtortillas/src/tracker/mod.rs b/crates/libtortillas/src/tracker/mod.rs index e2e3eef..e779637 100644 --- a/crates/libtortillas/src/tracker/mod.rs +++ b/crates/libtortillas/src/tracker/mod.rs @@ -312,6 +312,7 @@ impl Actor for TrackerActor { /// A message from an outside source. #[allow(dead_code)] +#[derive(Debug, Clone, Copy)] pub(crate) enum TrackerMessage { /// Forces the tracker to make an announce request. By default, announce /// requests are made on an interval. From 68f28e7eaa05711b62adcb045a2be0739c658767 Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Fri, 31 Oct 2025 11:27:51 -0700 Subject: [PATCH 07/16] refactor: Force announce after state update, then reset tracker state to `Empty` --- crates/libtortillas/src/torrent/actor.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/libtortillas/src/torrent/actor.rs b/crates/libtortillas/src/torrent/actor.rs index c21c82a..b3f5e9c 100644 --- a/crates/libtortillas/src/torrent/actor.rs +++ b/crates/libtortillas/src/torrent/actor.rs @@ -31,7 +31,7 @@ use crate::{ TorrentExport, piece_manager::{FilePieceManager, PieceManager}, }, - tracker::{Event, Tracker, TrackerActor, TrackerUpdate, udp::UdpServer}, + tracker::{Event, Tracker, TrackerActor, TrackerMessage, TrackerUpdate, udp::UdpServer}, }; pub const BLOCK_SIZE: usize = 16 * 1024; @@ -272,6 +272,16 @@ impl TorrentActor { .update_trackers(TrackerUpdate::Event(Event::Started)) .await; + // Force announce + self.broadcast_to_trackers(TrackerMessage::Announce).await; + + // Now apperently we're supposed to set our event back to "empty" for the next + // announce (done via the interval), no clue why, just the way it's + // specified in the spec. + self + .update_trackers(TrackerUpdate::Event(Event::Empty)) + .await; + // Request first piece from peers self .broadcast_to_peers(PeerTell::NeedPiece(self.next_piece, 0, BLOCK_SIZE)) @@ -503,6 +513,7 @@ impl TorrentActor { }); } } + pub(super) async fn broadcast_to_trackers(&self, message: TrackerMessage) { let trackers = self.trackers.clone(); From 15695ca812354c55cfdf5258f2337b913ba8dea3 Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Wed, 5 Nov 2025 21:33:14 -0800 Subject: [PATCH 08/16] feat: Add `total_bytes_downloaded` method on `TorrentActor` --- crates/libtortillas/src/torrent/actor.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/libtortillas/src/torrent/actor.rs b/crates/libtortillas/src/torrent/actor.rs index b3f5e9c..593f960 100644 --- a/crates/libtortillas/src/torrent/actor.rs +++ b/crates/libtortillas/src/torrent/actor.rs @@ -319,6 +319,21 @@ impl TorrentActor { ); } + /// Calculates the total number of bytes downloaded by the torrent. Returns + /// None if the info dict is not present. + pub fn total_bytes_downloaded(&self) -> Option { + let completed_pieces = self.bitfield.count_ones(); + let piece_size = self.info_dict().map(|info| info.piece_length)? as usize; + + let mut total_bytes = completed_pieces * piece_size; + + for block in self.block_map.iter() { + total_bytes += BLOCK_SIZE * block.count_ones(); + } + + Some(total_bytes) + } + pub fn export(&self) -> TorrentExport { TorrentExport { info_hash: self.info_hash(), From c56b4eedc925cd2df07936928cd13eca1ad16f33 Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Wed, 5 Nov 2025 21:39:21 -0800 Subject: [PATCH 09/16] feat: Change interval in forced tracker announce message --- crates/libtortillas/src/tracker/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/libtortillas/src/tracker/mod.rs b/crates/libtortillas/src/tracker/mod.rs index e779637..21ae2fb 100644 --- a/crates/libtortillas/src/tracker/mod.rs +++ b/crates/libtortillas/src/tracker/mod.rs @@ -334,6 +334,12 @@ impl Message for TrackerActor { Ok(peers) => { if let Err(e) = self.supervisor.tell(TorrentMessage::Announce(peers)).await { error!("Failed to send announce to supervisor: {}", e); + } else { + self.interval = interval(Duration::from_secs(self.tracker.interval() as u64)); + // Tick because when starting a new interval, it will tick immediately and + // cause a never ending loop, adding this doesn't add any delay and fixes that + // issue + self.interval.tick().await; } } Err(e) => { From b90230c66a5bf57f7001a379607b3d9a7e0320cc Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Wed, 5 Nov 2025 21:45:28 -0800 Subject: [PATCH 10/16] refactor: Move announce logic to message handler in `TrackerActor` --- crates/libtortillas/src/tracker/mod.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/crates/libtortillas/src/tracker/mod.rs b/crates/libtortillas/src/tracker/mod.rs index 21ae2fb..0deba76 100644 --- a/crates/libtortillas/src/tracker/mod.rs +++ b/crates/libtortillas/src/tracker/mod.rs @@ -14,7 +14,7 @@ use atomic_time::{AtomicInstant, AtomicOptionInstant}; use http::HttpTracker; use kameo::{ Actor, - actor::{ActorRef, WeakActorRef}, + actor::{ActorId, ActorRef, WeakActorRef}, error::ActorStopReason, mailbox::Signal, prelude::{Context, Message}, @@ -26,7 +26,7 @@ use serde::{ }; use serde_repr::{Deserialize_repr, Serialize_repr}; use tokio::time::{Instant, Interval, interval, timeout}; -use tracing::{error, warn}; +use tracing::{error, info, trace, warn}; use udp::UdpTracker; use crate::{ @@ -291,21 +291,19 @@ impl Actor for TrackerActor { } async fn next( - &mut self, _: kameo::prelude::WeakActorRef, + &mut self, actor_ref: kameo::prelude::WeakActorRef, mailbox_rx: &mut kameo::prelude::MailboxReceiver, ) -> Option> { tokio::select! { signal = mailbox_rx.recv() => signal, // Waits for the next interval to tick _ = self.interval.tick() => { - if let Ok(peers) = self.tracker.announce().await { - let _ = self.supervisor.tell(TorrentMessage::Announce(peers)).await; - } - let duration = Duration::from_secs(self.tracker.interval() as u64); - self.interval = interval(duration); - - None - } + let msg = TrackerMessage::Announce; + Some(Signal::Message{ message: Box::new(msg), + actor_ref: actor_ref.upgrade()?.clone(), + reply: None, + sent_within_actor: true, + })} } } } From 5de8e0be0ea5cec8b2c8b5b462612157d960e40b Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Wed, 5 Nov 2025 21:46:36 -0800 Subject: [PATCH 11/16] feat: Update trackers with total bytes downloaded --- crates/libtortillas/src/torrent/messages.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/libtortillas/src/torrent/messages.rs b/crates/libtortillas/src/torrent/messages.rs index 88efa1e..22e11a7 100644 --- a/crates/libtortillas/src/torrent/messages.rs +++ b/crates/libtortillas/src/torrent/messages.rs @@ -25,7 +25,7 @@ use crate::{ peer::{Peer, PeerId, PeerTell}, protocol::stream::PeerStream, torrent::{PieceManagerProxy, piece_manager::PieceManager}, - tracker::Tracker, + tracker::{Event, Tracker, TrackerMessage, TrackerUpdate}, }; /// For incoming from outside sources (e.g Peers, Trackers and Engine) @@ -196,6 +196,8 @@ impl Message for TorrentActor { .info_dict() .expect("Can't receive piece without info dict"); + let total_length = info_dict.total_length(); + let block_index = offset / BLOCK_SIZE; let piece_count = info_dict.piece_count(); @@ -296,6 +298,14 @@ impl Message for TorrentActor { // Announce to peers that we have this piece self.broadcast_to_peers(PeerTell::Have(cur_piece)).await; + + if let Some(total_downloaded) = self.total_bytes_downloaded() { + let total_bytes_left = total_length - total_downloaded; + self + .update_trackers(TrackerUpdate::Left(total_bytes_left)) + .await; + } + if self.next_piece >= piece_count - 1 { // Handle end of torrenting process self.state = TorrentState::Seeding; From e8661efa915b97433996a10f98a1c9fd8adf77b1 Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Wed, 5 Nov 2025 21:47:03 -0800 Subject: [PATCH 12/16] feat: Update trackers when we have completed torrent & force announce --- crates/libtortillas/src/torrent/messages.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/libtortillas/src/torrent/messages.rs b/crates/libtortillas/src/torrent/messages.rs index 22e11a7..ca4ed50 100644 --- a/crates/libtortillas/src/torrent/messages.rs +++ b/crates/libtortillas/src/torrent/messages.rs @@ -309,6 +309,13 @@ impl Message for TorrentActor { if self.next_piece >= piece_count - 1 { // Handle end of torrenting process self.state = TorrentState::Seeding; + + // Announce to trackers that we have completed the torrent + self + .update_trackers(TrackerUpdate::Event(Event::Completed)) + .await; + self.broadcast_to_trackers(TrackerMessage::Announce).await; + info!("Torrenting process completed, switching to seeding mode"); } else { self From 4d86616daddb2ccf772615b215c724d28f0f4c0a Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Wed, 5 Nov 2025 21:47:43 -0800 Subject: [PATCH 13/16] style: Fix clippy errors --- crates/libtortillas/src/torrent/actor.rs | 6 +++++- crates/libtortillas/src/tracker/mod.rs | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/libtortillas/src/torrent/actor.rs b/crates/libtortillas/src/torrent/actor.rs index 593f960..bb136e2 100644 --- a/crates/libtortillas/src/torrent/actor.rs +++ b/crates/libtortillas/src/torrent/actor.rs @@ -11,7 +11,11 @@ use async_trait::async_trait; use bitvec::vec::BitVec; use bytes::Bytes; use dashmap::DashMap; -use kameo::{Actor, actor::ActorRef, mailbox}; +use kameo::{ + Actor, + actor::ActorRef, + mailbox, +}; use librqbit_utp::UtpSocketUdp; use serde::{Deserialize, Serialize}; use tokio::sync::oneshot; diff --git a/crates/libtortillas/src/tracker/mod.rs b/crates/libtortillas/src/tracker/mod.rs index 0deba76..fea97dd 100644 --- a/crates/libtortillas/src/tracker/mod.rs +++ b/crates/libtortillas/src/tracker/mod.rs @@ -14,7 +14,7 @@ use atomic_time::{AtomicInstant, AtomicOptionInstant}; use http::HttpTracker; use kameo::{ Actor, - actor::{ActorId, ActorRef, WeakActorRef}, + actor::{ActorRef, WeakActorRef}, error::ActorStopReason, mailbox::Signal, prelude::{Context, Message}, @@ -26,7 +26,7 @@ use serde::{ }; use serde_repr::{Deserialize_repr, Serialize_repr}; use tokio::time::{Instant, Interval, interval, timeout}; -use tracing::{error, info, trace, warn}; +use tracing::{error, warn}; use udp::UdpTracker; use crate::{ From 31037cc49d27f98e23a284f158e6ab96aebc0b6b Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Wed, 5 Nov 2025 21:57:54 -0800 Subject: [PATCH 14/16] style: Fix fmt errors --- crates/libtortillas/src/torrent/actor.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/libtortillas/src/torrent/actor.rs b/crates/libtortillas/src/torrent/actor.rs index bb136e2..593f960 100644 --- a/crates/libtortillas/src/torrent/actor.rs +++ b/crates/libtortillas/src/torrent/actor.rs @@ -11,11 +11,7 @@ use async_trait::async_trait; use bitvec::vec::BitVec; use bytes::Bytes; use dashmap::DashMap; -use kameo::{ - Actor, - actor::ActorRef, - mailbox, -}; +use kameo::{Actor, actor::ActorRef, mailbox}; use librqbit_utp::UtpSocketUdp; use serde::{Deserialize, Serialize}; use tokio::sync::oneshot; From 49ccc382e326966ac8b3527b94b6bd0cbbef9ed5 Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Thu, 6 Nov 2025 12:04:33 -0800 Subject: [PATCH 15/16] fix: Stop panic from 0 interval --- crates/libtortillas/src/tracker/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/libtortillas/src/tracker/mod.rs b/crates/libtortillas/src/tracker/mod.rs index fea97dd..0fdfac8 100644 --- a/crates/libtortillas/src/tracker/mod.rs +++ b/crates/libtortillas/src/tracker/mod.rs @@ -333,7 +333,10 @@ impl Message for TrackerActor { if let Err(e) = self.supervisor.tell(TorrentMessage::Announce(peers)).await { error!("Failed to send announce to supervisor: {}", e); } else { - self.interval = interval(Duration::from_secs(self.tracker.interval() as u64)); + // Ensure we don't have a delay of 0 + let delay = self.tracker.interval().max(1) as u64; + + self.interval = interval(Duration::from_secs(delay)); // Tick because when starting a new interval, it will tick immediately and // cause a never ending loop, adding this doesn't add any delay and fixes that // issue From 3dc87d25e6d9726a30477a14e2b2e7660eaedd5c Mon Sep 17 00:00:00 2001 From: artrixdotdev Date: Fri, 7 Nov 2025 23:02:48 -0800 Subject: [PATCH 16/16] fix: Correct byte counting in `total_bytes_downloaded` for partial blocks --- crates/libtortillas/src/torrent/actor.rs | 51 +++++++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/crates/libtortillas/src/torrent/actor.rs b/crates/libtortillas/src/torrent/actor.rs index 593f960..9795064 100644 --- a/crates/libtortillas/src/torrent/actor.rs +++ b/crates/libtortillas/src/torrent/actor.rs @@ -322,18 +322,57 @@ impl TorrentActor { /// Calculates the total number of bytes downloaded by the torrent. Returns /// None if the info dict is not present. pub fn total_bytes_downloaded(&self) -> Option { - let completed_pieces = self.bitfield.count_ones(); - let piece_size = self.info_dict().map(|info| info.piece_length)? as usize; + let info = self.info_dict()?; + let total_length = info.total_length(); + let piece_length = info.piece_length as usize; - let mut total_bytes = completed_pieces * piece_size; + let num_pieces = self.bitfield.len(); + let mut total_bytes = 0usize; - for block in self.block_map.iter() { - total_bytes += BLOCK_SIZE * block.count_ones(); + // Calculate the size of the last piece + let last_piece_len = if total_length % piece_length == 0 { + piece_length + } else { + total_length % piece_length + }; + + // Sum bytes from completed pieces + for piece_idx in 0..num_pieces { + if self.bitfield[piece_idx] { + let piece_size = if piece_idx == num_pieces - 1 { + last_piece_len + } else { + piece_length + }; + total_bytes = total_bytes.saturating_add(piece_size); + } + } + + // Sum bytes from incomplete pieces via block_map + for (piece_idx, block) in self.block_map.iter().enumerate() { + if piece_idx < num_pieces && !self.bitfield[piece_idx] { + let piece_size = if piece_idx == num_pieces - 1 { + last_piece_len + } else { + piece_length + }; + + let mut piece_offset = 0usize; + for block_idx in 0..block.len() { + if block[block_idx] { + let block_size = (piece_size - piece_offset).min(BLOCK_SIZE); + total_bytes = total_bytes.saturating_add(block_size); + piece_offset = piece_offset.saturating_add(block_size); + } else { + piece_offset = + piece_offset.saturating_add(BLOCK_SIZE.min(piece_size - piece_offset)); + } + } + } } Some(total_bytes) } - pub fn export(&self) -> TorrentExport { TorrentExport { info_hash: self.info_hash(),