Skip to main content

aria2_core/engine/
bt_download_command.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3use std::time::{Duration, Instant};
4use tracing::{debug, info, warn};
5
6use crate::constants;
7use crate::engine::bt_choke_manager::{
8    add_peer_to_tracking, check_snubbed_peers, handle_snubbed_peer, on_data_received_from_peer,
9    on_peer_choke, on_peer_unchoke, on_piece_received, select_best_peer_for_request,
10};
11use crate::engine::bt_post_download_handler::HookManager;
12use crate::engine::bt_progress_info_file::BtProgressManager;
13use crate::engine::bt_tracker_comm::announce_to_public_tracker;
14use crate::engine::choking_algorithm::{ChokingAlgorithm, ChokingConfig};
15use crate::engine::http_tracker_client::TrackerState;
16use crate::engine::lpd_manager::LpdManager;
17use crate::engine::multi_file_layout::MultiFileLayout;
18use crate::error::{Aria2Error, FatalError, Result};
19use crate::filesystem::file_lock::DownloadPathLock;
20use crate::request::request_group::{DownloadOptions, GroupId, RequestGroup};
21
22pub use crate::engine::bt_message_handler::{
23    BLOCK_REQUEST_TIMEOUT_SECS, BLOCK_SIZE, MAX_BLOCK_READ_MESSAGES, MAX_RETRIES,
24};
25pub use crate::engine::bt_peer_interaction::{
26    MAX_UNCHOKE_WAIT_ATTEMPTS, PEER_CONNECTION_DELAY_MS, PEER_MESSAGE_TIMEOUT_SECS,
27};
28pub use crate::engine::bt_piece_selector::ENDGAME_THRESHOLD;
29
30pub(crate) const PUBLIC_TRACKER_PEER_THRESHOLD: usize = 15;
31pub(crate) const MAX_PUBLIC_TRACKERS_TO_TRY: usize = 10;
32
33pub struct BtDownloadCommand {
34    pub(crate) group: Arc<tokio::sync::RwLock<RequestGroup>>,
35    pub(crate) output_path: std::path::PathBuf,
36    pub(crate) started: bool,
37    pub(crate) completed_bytes: u64,
38    pub(crate) torrent_data: Vec<u8>,
39    pub(crate) seed_enabled: bool,
40    pub(crate) seed_time: Option<std::time::Duration>,
41    pub(crate) seed_ratio: Option<f64>,
42    pub(crate) total_uploaded: u64,
43    pub(crate) udp_client: Option<crate::engine::udp_tracker_client::SharedUdpClient>,
44    pub(crate) dht_engine:
45        Option<std::sync::Arc<aria2_protocol::bittorrent::dht::engine::DhtEngine>>,
46    pub(crate) public_trackers:
47        Option<std::sync::Arc<aria2_protocol::bittorrent::tracker::public_list::PublicTrackerList>>,
48    pub choking_algo: Option<ChokingAlgorithm>,
49    pub multi_file_layout: Option<MultiFileLayout>,
50
51    // P1/P2 集成字段(全部使用 Option 保持向后兼容)
52    /// BT进度持久化管理器
53    pub(crate) progress_manager: Option<BtProgressManager>,
54    /// 进度保存间隔(默认60秒)
55    pub(crate) progress_save_interval: Duration,
56    /// LPD局域网peer发现管理器
57    pub(crate) lpd_manager: Option<Arc<LpdManager>>,
58    /// 下载后处理钩子管理器
59    pub(crate) hook_manager: Option<Arc<HookManager>>,
60
61    // PEX (Peer Exchange, BEP 11) integration fields
62    /// Track known peers for PEX exchange
63    pub(crate) pex_known_peers: Vec<aria2_protocol::bittorrent::peer::connection::PeerAddr>,
64    /// Timestamp of last PEX message sent (for rate limiting)
65    pub(crate) pex_last_send_time: Option<Instant>,
66    /// Interval between PEX messages (default 60 seconds)
67    pub(crate) pex_send_interval: Duration,
68
69    // Endgame mode (Phase 14 - B1/B2): duplicate request tracking for final pieces
70    /// Tracks duplicate block requests during endgame mode
71    pub(crate) endgame_state: super::bt_download_execute::EndgameState,
72
73    // BEP 6 (Fast Extension): track AllowedFast messages sent to peers
74    /// Track which AllowedFast pieces have been sent to each peer
75    /// Key: peer identifier (using connection index for now)
76    #[allow(dead_code)]
77    pub(crate) allowed_fast_sent_peers: HashMap<usize, HashSet<u32>>,
78
79    /// Track suggest counts per peer to avoid spamming
80    pub(crate) suggest_sent_counts: HashMap<usize, usize>,
81
82    // Tracker event state machine (Phase 15 - H5): manages Started/Completed/Stopped events
83    /// State machine for tracker announce events
84    #[allow(dead_code)]
85    pub(crate) tracker_state: TrackerState,
86
87    // Web-seed (BEP 19 / HTTP fallback) integration
88    /// URLs extracted from torrent's url-list field for HTTP piece download fallback
89    pub(crate) web_seed_urls: Vec<String>,
90    /// Web seed manager for HTTP piece downloads (initialized on first use)
91    pub(crate) web_seed_manager: Option<crate::engine::bt_web_seed::WebSeedManager>,
92
93    // File lock (J6): prevents concurrent aria2 instances from writing to same output dir
94    /// Download path lock held for the lifetime of this command.
95    /// Prevents other aria2 instances from writing to the same output directory.
96    pub download_path_lock: Option<DownloadPathLock>,
97
98    // Seeding mode (Phase 16 - Complete BitTorrent seeding)
99    /// Seed manager for uploading after download completes
100    pub(crate) seed_manager: Option<super::bt_seed_manager::BtSeedManager>,
101
102    // BEP 0027 (Private Torrent): when true, DHT/PEX/LPD and public tracker
103    // announcement are disabled to enforce the privacy guarantees of the
104    // torrent's `private` flag.
105    pub(crate) is_private: bool,
106}
107
108impl BtDownloadCommand {
109    pub fn new(
110        gid: GroupId,
111        torrent_bytes: &[u8],
112        options: &DownloadOptions,
113        output_dir: Option<&str>,
114    ) -> Result<Self> {
115        let meta = aria2_protocol::bittorrent::torrent::parser::TorrentMeta::parse(torrent_bytes)
116            .map_err(|e| {
117            Aria2Error::Fatal(FatalError::Config(format!("Torrent parse failed: {}", e)))
118        })?;
119
120        // BEP 0027 (Private Torrent): capture the private flag at parse time.
121        // When true, the engine must disable DHT, PEX, LPD and public tracker
122        // announcement to honour the privacy contract.
123        let is_private = meta.is_private();
124        if is_private {
125            info!(
126                "[BT] Private torrent detected (BEP 0027): DHT/PEX/LPD and public trackers will be disabled"
127            );
128        }
129
130        let dir = output_dir
131            .map(|d| d.to_string())
132            .or_else(|| options.dir.clone())
133            .unwrap_or_else(|| ".".to_string());
134
135        let filename = meta.info.name.clone();
136        let path = std::path::PathBuf::from(&dir).join(&filename);
137
138        let group = RequestGroup::new(
139            gid,
140            vec![format!("bt://{}", meta.info_hash.as_hex())],
141            options.clone(),
142        );
143
144        // Set BT metadata for session persistence (Task 3)
145        group.set_bt_metadata(
146            meta.num_pieces() as u32,
147            meta.info.piece_length,
148            meta.info_hash.as_hex(),
149        );
150
151        let seed_time = options.seed_time.and_then(|t| {
152            if t == 0 {
153                None
154            } else {
155                Some(std::time::Duration::from_secs(t))
156            }
157        });
158        let seed_ratio = options.seed_ratio.filter(|&r| r > 0.0);
159
160        info!(
161            "BtDownloadCommand created: {} -> {} ({} bytes, {} pieces) seed={:?} ratio={:?}",
162            meta.info.name,
163            path.display(),
164            meta.total_size(),
165            meta.num_pieces(),
166            seed_time,
167            seed_ratio
168        );
169
170        let choking_algo = if options.bt_max_upload_slots.is_some()
171            || options.bt_optimistic_unchoke_interval.is_some()
172            || options.bt_snubbed_timeout.is_some()
173        {
174            let config = ChokingConfig {
175                max_upload_slots: options
176                    .bt_max_upload_slots
177                    .unwrap_or(constants::BT_DEFAULT_MAX_UPLOAD_SLOTS as u32)
178                    as usize,
179                optimistic_unchoke_interval_secs: options
180                    .bt_optimistic_unchoke_interval
181                    .unwrap_or(constants::BT_OPTIMISTIC_UNCHOKE_INTERVAL_SECS),
182                snubbed_timeout_secs: options
183                    .bt_snubbed_timeout
184                    .unwrap_or(constants::BT_SNUBBED_TIMEOUT_SECS),
185                choke_rotation_interval_secs: constants::BT_CHOKE_ROTATION_INTERVAL_SECS,
186            };
187            Some(ChokingAlgorithm::new(config))
188        } else {
189            None
190        };
191
192        let multi_file_layout = if !meta.is_single_file() {
193            let layout_base_dir = std::path::PathBuf::from(&dir);
194            match MultiFileLayout::from_info_dict(&meta.info, &layout_base_dir) {
195                Ok(layout) => Some(layout),
196                Err(e) => {
197                    return Err(Aria2Error::Fatal(FatalError::Config(format!(
198                        "MultiFileLayout creation failed: {}",
199                        e
200                    ))));
201                }
202            }
203        } else {
204            None
205        };
206
207        let effective_output_path = if multi_file_layout.is_some() {
208            std::path::PathBuf::from(&dir)
209        } else {
210            path.clone()
211        };
212
213        info!(
214            "BtDownloadCommand created: {} -> {} ({} bytes, {} pieces) seed={:?} ratio={:?} multi_file={}",
215            meta.info.name,
216            effective_output_path.display(),
217            meta.total_size(),
218            meta.num_pieces(),
219            seed_time,
220            seed_ratio,
221            multi_file_layout.is_some()
222        );
223
224        // Acquire download path lock (J6): prevents concurrent instances from
225        // writing to the same output directory. If acquisition fails, log a
226        // warning but do not fail the download -- the lock is a best-effort guard.
227        // NOTE: always pass the output DIRECTORY, not the file path. For
228        // single-file torrents `effective_output_path` is `dir/filename` (a file
229        // path); passing it to acquire_for_download would cause create_dir_all to
230        // create `filename` as a directory, which then makes File::create fail
231        // with "Access denied" (os error 5) on Windows.
232        let download_path_lock =
233            match DownloadPathLock::acquire_for_download(std::path::Path::new(&dir)) {
234                Ok(lock) => Some(lock),
235                Err(e) => {
236                    warn!(
237                        "Failed to acquire download path lock: {}. Proceeding without lock.",
238                        e
239                    );
240                    None
241                }
242            };
243
244        Ok(Self {
245            group: Arc::new(tokio::sync::RwLock::new(group)),
246            output_path: effective_output_path,
247            started: false,
248            completed_bytes: 0,
249            torrent_data: torrent_bytes.to_vec(),
250            seed_enabled: options.seed_time.unwrap_or(0) > 0
251                || options.seed_ratio.unwrap_or(0.0) > 0.0,
252            seed_time,
253            seed_ratio,
254            total_uploaded: 0,
255            udp_client: None,
256            dht_engine: None,
257            public_trackers: None,
258            choking_algo,
259            multi_file_layout,
260
261            // P1/P2 集成字段默认值(全部为 None,保持向后兼容)
262            progress_manager: None,
263            progress_save_interval: Duration::from_secs(60),
264            lpd_manager: None,
265            hook_manager: None,
266
267            // PEX integration fields default values
268            pex_known_peers: Vec::new(),
269            pex_last_send_time: None,
270            pex_send_interval: Duration::from_secs(60),
271
272            // BEP 6 Fast Extension tracking
273            allowed_fast_sent_peers: HashMap::new(),
274            suggest_sent_counts: HashMap::new(),
275
276            // Endgame mode default values
277            endgame_state: super::bt_download_execute::EndgameState::new(),
278
279            // Tracker event state machine default
280            tracker_state: TrackerState::new(),
281
282            // Web-seed URLs (extracted from torrent url-list field)
283            web_seed_urls: meta.web_seeds.clone(),
284            // Web seed manager (initialized lazily when needed)
285            web_seed_manager: None,
286
287            // Download path lock (J6)
288            download_path_lock,
289
290            // Seeding mode
291            seed_manager: None,
292
293            // BEP 0027 (Private Torrent) enforcement flag
294            is_private,
295        })
296    }
297
298    pub async fn group(&self) -> tokio::sync::RwLockReadGuard<'_, RequestGroup> {
299        self.group.read().await
300    }
301
302    pub fn on_peer_choke(&mut self, peer_idx: usize) {
303        on_peer_choke(&mut self.choking_algo, peer_idx);
304    }
305
306    pub fn on_peer_unchoke(&mut self, peer_idx: usize) {
307        on_peer_unchoke(&mut self.choking_algo, peer_idx);
308    }
309
310    pub fn on_data_received_from_peer(&mut self, peer_idx: usize, bytes: u64) {
311        on_data_received_from_peer(&mut self.choking_algo, peer_idx, bytes);
312    }
313
314    pub fn check_snubbed_peers(&mut self) -> Vec<usize> {
315        check_snubbed_peers(&mut self.choking_algo)
316    }
317
318    pub fn add_peer_to_tracking(&mut self, peer_id: [u8; 8], addr: std::net::SocketAddr) -> usize {
319        add_peer_to_tracking(&mut self.choking_algo, peer_id, addr)
320    }
321
322    pub fn select_best_peer_for_request(&self) -> Option<usize> {
323        select_best_peer_for_request(&self.choking_algo)
324    }
325
326    pub async fn handle_snubbed_peer(&mut self, peer_idx: usize) -> Result<()> {
327        handle_snubbed_peer(&mut self.choking_algo, peer_idx)
328            .await
329            .map_err(|_| {
330                Aria2Error::Fatal(FatalError::Config(format!(
331                    "Failed to handle snubbed peer {}",
332                    peer_idx
333                )))
334            })
335    }
336
337    pub fn on_piece_received(&mut self, peer_idx: usize, bytes: u64) {
338        on_piece_received(&mut self.choking_algo, peer_idx, bytes);
339    }
340
341    /// Explicitly mark a peer as snubbed (algorithm-level snubbing).
342    ///
343    /// This adds the peer to the explicit snubbed set, causing them to receive
344    /// a score of -1000 on the next choke rotation, ensuring they are always choked.
345    pub fn mark_peer_snubbed(&mut self, peer_idx: usize) {
346        if let Some(algo) = &mut self.choking_algo {
347            algo.mark_peer_snubbed(peer_idx);
348        }
349    }
350
351    /// Check if a peer is explicitly snubbed at the algorithm level.
352    pub fn is_explicitly_snubbed(&self, peer_idx: usize) -> bool {
353        self.choking_algo
354            .as_ref()
355            .map(|a| a.is_explicitly_snubbed(peer_idx))
356            .unwrap_or(false)
357    }
358
359    pub async fn announce_to_public_tracker(
360        tracker_url: &str,
361        info_hash: &[u8; 20],
362        peer_id: &[u8; 20],
363        total_size: u64,
364    ) -> std::result::Result<Vec<(String, u16)>, String> {
365        announce_to_public_tracker(tracker_url, info_hash, peer_id, total_size).await
366    }
367
368    /// Wrapper around [`crate::engine::bt_piece_downloader::write_piece_to_multi_files`].
369    pub async fn write_piece_to_multi_files(
370        layout: &MultiFileLayout,
371        piece_idx: u32,
372        piece_data: &[u8],
373        piece_length: u32,
374    ) -> Result<()> {
375        crate::engine::bt_piece_downloader::write_piece_to_multi_files(
376            layout,
377            piece_idx,
378            piece_data,
379            piece_length,
380        )
381        .await
382    }
383
384    /// Wrapper around [`crate::engine::bt_piece_downloader::write_piece_to_multi_files_coalesced`].
385    ///
386    /// Prefer this over `write_piece_to_multi_files` for production use — it
387    /// merges adjacent writes within a 4 KiB gap, reducing syscall count.
388    pub async fn write_piece_to_multi_files_coalesced(
389        layout: &MultiFileLayout,
390        piece_idx: u32,
391        piece_data: &[u8],
392        piece_length: u32,
393    ) -> Result<()> {
394        crate::engine::bt_piece_downloader::write_piece_to_multi_files_coalesced(
395            layout,
396            piece_idx,
397            piece_data,
398            piece_length,
399        )
400        .await
401    }
402
403    pub fn is_multi_file(&self) -> bool {
404        self.multi_file_layout
405            .as_ref()
406            .is_some_and(|l| l.is_multi_file())
407    }
408
409    pub fn get_multi_file_layout(&self) -> Option<&MultiFileLayout> {
410        self.multi_file_layout.as_ref()
411    }
412
413    // ==================== P1/P2 集成 API ====================
414
415    /// 设置 BT 进度管理器
416    ///
417    /// Enable BT download progress persistence for resume support.
418    ///
419    /// When enabled, the engine periodically saves piece completion bitfield,
420    /// peer list, and download statistics to a `.aria2` file in INI format.
421    /// On restart, the progress is loaded to skip already-completed pieces.
422    ///
423    /// # Arguments
424    ///
425    /// * `manager` - An initialized [`BtProgressManager`](super::bt_progress_info_file::BtProgressManager) instance
426    ///
427    /// # Example
428    ///
429    /// ```rust,no_run
430    /// use aria2_core::engine::bt_progress_info_file::BtProgressManager;
431    /// use std::path::PathBuf;
432    ///
433    /// let save_dir = PathBuf::from("/tmp/aria2");
434    /// let progress_mgr = BtProgressManager::new(&save_dir).expect("failed to create progress manager");
435    /// // Pass progress_mgr to BtDownloadCommand::set_progress_manager()
436    /// let _mgr = progress_mgr;
437    /// ```
438    pub fn set_progress_manager(&mut self, manager: BtProgressManager) {
439        info!("BT progress manager enabled");
440        self.progress_manager = Some(manager);
441    }
442
443    /// Set the interval (in seconds) between progress save operations.
444    ///
445    /// # Arguments
446    ///
447    /// * `interval_secs` - Save interval in seconds (default: 60)
448    pub fn set_progress_save_interval(&mut self, interval_secs: u64) {
449        self.progress_save_interval = Duration::from_secs(interval_secs);
450        info!(interval_secs, "Progress save interval updated");
451    }
452
453    /// Enable Local Peer Discovery (LPD, BEP 14) for LAN peer finding.
454    ///
455    /// When enabled, the engine announces its active downloads via UDP multicast
456    /// to `239.192.152.143:6771` and listens for peers on the same network.
457    ///
458    /// # Arguments
459    ///
460    /// * `manager` - An initialized [`LpdManager`](super::lpd_manager::LpdManager), wrapped in `Arc`
461    pub fn set_lpd_manager(&mut self, manager: Arc<LpdManager>) {
462        info!("LPD manager enabled for local peer discovery");
463        self.lpd_manager = Some(manager);
464    }
465
466    /// Register a post-download hook chain for completion/error callbacks.
467    ///
468    /// Hooks execute sequentially after download completes or fails.
469    /// Built-in hook types: Move, Rename, Touch, Exec (shell command).
470    /// A single hook failure does not block subsequent hooks.
471    ///
472    /// # Arguments
473    ///
474    /// * `manager` - A configured [`HookManager`](super::bt_post_download_handler::HookManager) with registered hooks, wrapped in `Arc`
475    ///
476    /// # Example
477    ///
478    /// ```rust,no_run
479    /// use std::collections::HashMap;
480    /// use std::sync::Arc;
481    /// use aria2_core::engine::bt_post_download_handler::{HookManager, HookConfig, MoveHook, ExecHook};
482    ///
483    /// let config = HookConfig::default();
484    /// let mut hooks = HookManager::new(config);
485    /// hooks.add_hook(Box::new(MoveHook::new("/completed".into(), true)));
486    /// hooks.add_hook(Box::new(ExecHook::new("notify.sh".into(), HashMap::new())));
487    /// // Pass Arc::new(hooks) to BtDownloadCommand::set_hook_manager()
488    /// let _hooks = Arc::new(hooks);
489    /// ```
490    pub fn set_hook_manager(&mut self, manager: Arc<HookManager>) {
491        info!(
492            hook_count = manager.hook_count(),
493            "Hook manager enabled with {} hooks",
494            manager.hook_count()
495        );
496        self.hook_manager = Some(manager);
497    }
498
499    /// 获取进度管理器引用(用于测试和外部访问)
500    pub fn get_progress_manager(&self) -> Option<&BtProgressManager> {
501        self.progress_manager.as_ref()
502    }
503
504    /// 获取 LPD 管理器引用(用于测试和外部访问)
505    pub fn get_lpd_manager(&self) -> Option<&Arc<LpdManager>> {
506        self.lpd_manager.as_ref()
507    }
508
509    /// 获取钩子管理器引用(用于测试和外部访问)
510    pub fn get_hook_manager(&self) -> Option<&Arc<HookManager>> {
511        self.hook_manager.as_ref()
512    }
513
514    // ==================== PEX (BEP 11) Integration API ====================
515
516    /// Add a peer address to the known peers list for PEX exchange
517    pub fn add_pex_peer(
518        &mut self,
519        peer_addr: aria2_protocol::bittorrent::peer::connection::PeerAddr,
520    ) {
521        if !self.pex_known_peers.contains(&peer_addr) {
522            debug!(addr = %format!("{}:{}", peer_addr.ip, peer_addr.port), "Adding peer to PEX known list");
523            self.pex_known_peers.push(peer_addr);
524        }
525    }
526
527    /// Set the list of known peers for PEX exchange
528    pub fn set_pex_known_peers(
529        &mut self,
530        peers: Vec<aria2_protocol::bittorrent::peer::connection::PeerAddr>,
531    ) {
532        self.pex_known_peers = peers;
533        info!(
534            count = self.pex_known_peers.len(),
535            "PEX known peers updated"
536        );
537    }
538
539    /// Get reference to PEX known peers list
540    pub fn get_pex_known_peers(&self) -> &[aria2_protocol::bittorrent::peer::connection::PeerAddr] {
541        &self.pex_known_peers
542    }
543
544    /// Set custom PEX send interval (default 60 seconds)
545    pub fn set_pex_send_interval(&mut self, interval_secs: u64) {
546        self.pex_send_interval = Duration::from_secs(interval_secs);
547        info!(interval_secs, "PEX send interval updated");
548    }
549
550    /// Check if it's time to send a PEX message based on rate limiting
551    pub fn should_send_pex(&self) -> bool {
552        match self.pex_last_send_time {
553            Some(last) => last.elapsed() >= self.pex_send_interval,
554            None => true,
555        }
556    }
557
558    /// Update the last PEX send timestamp
559    pub fn update_pex_last_send(&mut self) {
560        self.pex_last_send_time = Some(Instant::now());
561    }
562
563    // ==================== Endgame Mode (Phase 14 - B1/B2) API ====================
564
565    /// Get a mutable reference to the EndgameState for tracking duplicate requests
566    pub fn endgame_state_mut(&mut self) -> &mut super::bt_download_execute::EndgameState {
567        &mut self.endgame_state
568    }
569
570    /// Get an immutable reference to the EndgameState
571    pub fn endgame_state(&self) -> &super::bt_download_execute::EndgameState {
572        &self.endgame_state
573    }
574
575    // ==================== H3: Bad Peer Detection / Ban System API ====================
576
577    /// Record that a specific peer sent invalid piece data (hash verification failed).
578    ///
579    /// This method:
580    /// 1. Increments the peer's `bad_data_count` in the choking algorithm's PeerStats
581    /// 2. If the count reaches [`crate::engine::peer_stats::BAD_DATA_THRESHOLD`],
582    ///    automatically bans the peer with a reason message
583    /// 3. Logs the event at WARN level
584    ///
585    /// # Arguments
586    ///
587    /// * `peer_idx` - The index of the peer in the choking algorithm's peer list
588    /// * `piece_index` - The index of the piece that failed verification
589    ///
590    /// # Returns
591    ///
592    /// * `Ok(true)` if the peer was banned as a result of this call
593    /// * `Ok(false)` if the peer was not banned (count below threshold)
594    /// * `Err(())` if the peer index is invalid or choking algorithm is not configured
595    #[allow(clippy::result_unit_err)]
596    pub fn record_bad_piece_for_peer(
597        &mut self,
598        peer_idx: usize,
599        piece_index: u32,
600    ) -> std::result::Result<bool, ()> {
601        use crate::engine::peer_stats::BAD_DATA_THRESHOLD;
602
603        if let Some(ref mut algo) = self.choking_algo
604            && let Some(peer) = algo.get_peer_mut(peer_idx)
605        {
606            let should_ban = peer.increment_bad_data();
607
608            warn!(
609                "[BT] Peer {} sent invalid data for piece {} (bad count: {}/{})",
610                peer_idx, piece_index, peer.bad_data_count, BAD_DATA_THRESHOLD
611            );
612
613            if should_ban {
614                let reason = format!(
615                    "Too many invalid pieces ({} >= {})",
616                    peer.bad_data_count, BAD_DATA_THRESHOLD
617                );
618                warn!("[BT] BANNING peer {}: {}", peer_idx, reason);
619                peer.ban_peer(reason);
620                return Ok(true); // Peer was banned
621            }
622
623            return Ok(false); // Count incremented but not banned yet
624        }
625
626        Err(()) // Invalid peer index or no choking algorithm
627    }
628
629    /// Record that a valid, verified piece was received from a peer.
630    ///
631    /// This triggers gradual recovery by decrementing the peer's `bad_data_count`.
632    /// Call this after successful hash verification to allow peers to recover reputation.
633    ///
634    /// # Arguments
635    ///
636    /// * `peer_idx` - The index of the peer in the choking algorithm's peer list
637    pub fn record_valid_piece_for_peer(&mut self, peer_idx: usize) {
638        if let Some(ref mut algo) = self.choking_algo
639            && let Some(peer) = algo.get_peer_mut(peer_idx)
640        {
641            peer.decrement_bad_data();
642            debug!(
643                "[BT] Peer {} sent valid piece, bad count decremented to {}",
644                peer_idx, peer.bad_data_count
645            );
646        }
647    }
648
649    /// Check if a peer is currently banned.
650    ///
651    /// # Arguments
652    ///
653    /// * `peer_idx` - The index of the peer in the choking algorithm's peer list
654    ///
655    /// # Returns
656    ///
657    /// * `true` if the peer is banned or peer not found
658    /// * `false` if the peer exists and is not banned
659    pub fn is_peer_banned(&self, peer_idx: usize) -> bool {
660        self.choking_algo
661            .as_ref()
662            .and_then(|algo| algo.get_peer(peer_idx))
663            .map(|p| p.is_banned)
664            .unwrap_or(true) // If not found, treat as banned for safety
665    }
666
667    /// Get a reference to a peer's stats for RPC/display purposes.
668    ///
669    /// Returns `None` if the peer doesn't exist or no choking algorithm is configured.
670    pub fn get_peer_stats(&self, peer_idx: usize) -> Option<&crate::engine::peer_stats::PeerStats> {
671        self.choking_algo.as_ref()?.get_peer(peer_idx)
672    }
673
674    // ==================== Web Seed (BEP 19) Integration API ====================
675
676    /// Initialize the web seed manager if web seeds are configured.
677    ///
678    /// This should be called after the torrent metadata is parsed and before
679    /// the download loop starts.
680    ///
681    /// # Arguments
682    ///
683    /// * `piece_length` - Length of each piece in the torrent
684    /// * `total_length` - Total file length
685    pub fn init_web_seed_manager(&mut self, piece_length: u32, total_length: u64) {
686        if !self.web_seed_urls.is_empty() && self.web_seed_manager.is_none() {
687            info!(
688                count = self.web_seed_urls.len(),
689                "Initializing web seed manager with {} URL(s)",
690                self.web_seed_urls.len()
691            );
692            self.web_seed_manager = Some(crate::engine::bt_web_seed::WebSeedManager::new(
693                self.web_seed_urls.clone(),
694                piece_length,
695                total_length,
696            ));
697        }
698    }
699
700    /// Get a reference to the web seed manager.
701    pub fn get_web_seed_manager(&self) -> Option<&crate::engine::bt_web_seed::WebSeedManager> {
702        self.web_seed_manager.as_ref()
703    }
704
705    /// Get a mutable reference to the web seed manager.
706    pub fn get_web_seed_manager_mut(
707        &mut self,
708    ) -> Option<&mut crate::engine::bt_web_seed::WebSeedManager> {
709        self.web_seed_manager.as_mut()
710    }
711
712    /// Check if web seeds are available.
713    pub fn has_web_seeds(&self) -> bool {
714        !self.web_seed_urls.is_empty()
715    }
716
717    /// Get web seed download statistics.
718    pub fn web_seed_stats(&self) -> Option<&crate::engine::bt_web_seed::WebSeedStats> {
719        self.web_seed_manager.as_ref().map(|m| m.stats())
720    }
721
722    // ==================== Seeding Mode (Phase 16) API ====================
723
724    /// Check if download is complete and start seeding if enabled.
725    ///
726    /// This method should be called after the download loop completes.
727    /// It initializes the seed manager if:
728    /// - All pieces are complete
729    /// - Seeding is enabled (seed_ratio > 0 or seed_time > 0)
730    ///
731    /// # Arguments
732    ///
733    /// * `piece_picker` - Reference to the piece picker to check completion
734    /// * `meta` - Torrent metadata
735    /// * `connections` - Active peer connections to use for seeding
736    /// * `piece_provider` - Provider for piece data (from downloaded files)
737    ///
738    /// # Returns
739    ///
740    /// * `Ok(true)` if seeding was started
741    /// * `Ok(false)` if seeding was not started (not complete or disabled)
742    pub fn check_and_start_seeding(
743        &mut self,
744        piece_picker: &aria2_protocol::bittorrent::piece::picker::PiecePicker,
745        meta: &aria2_protocol::bittorrent::torrent::parser::TorrentMeta,
746        connections: Vec<aria2_protocol::bittorrent::peer::connection::PeerConnection>,
747        piece_provider: std::sync::Arc<dyn crate::engine::bt_upload_session::PieceDataProvider>,
748    ) -> Result<bool> {
749        // Check if download is complete
750        if !piece_picker.is_complete() {
751            debug!("Download not complete, skipping seeding");
752            return Ok(false);
753        }
754
755        // Check if seeding is enabled
756        if !self.seed_enabled {
757            info!("Seeding disabled, not starting seed manager");
758            return Ok(false);
759        }
760
761        // Initialize seed manager
762        use crate::engine::bt_seed_manager::{BtSeedManager, SeedExitCondition};
763        use crate::engine::bt_upload_session::BtSeedingConfig;
764
765        let seed_ratio = self.seed_ratio.unwrap_or(0.0);
766        let seed_time = self.seed_time.map(|d| d.as_secs());
767
768        let exit_condition =
769            SeedExitCondition::with_time_and_ratio(seed_time.unwrap_or(0), seed_ratio);
770
771        let config = BtSeedingConfig {
772            max_upload_bytes_per_sec: None, // Will be set from options if needed
773            max_peers_to_unchoke: 4,
774            optimistic_unchoke_interval_secs: 30,
775        };
776
777        let total_downloaded = meta.total_size();
778
779        let seed_manager = BtSeedManager::new_with_info_hash(
780            meta.info_hash.bytes,
781            connections,
782            piece_provider,
783            config,
784            exit_condition,
785            total_downloaded,
786        );
787
788        self.seed_manager = Some(seed_manager);
789
790        info!(
791            "Seeding started: ratio={}, time={:?}, info_hash={}",
792            seed_ratio,
793            self.seed_time,
794            meta.info_hash.as_hex()
795        );
796
797        Ok(true)
798    }
799
800    /// Get a reference to the seed manager.
801    pub fn get_seed_manager(&self) -> Option<&super::bt_seed_manager::BtSeedManager> {
802        self.seed_manager.as_ref()
803    }
804
805    /// Get a mutable reference to the seed manager.
806    pub fn get_seed_manager_mut(&mut self) -> Option<&mut super::bt_seed_manager::BtSeedManager> {
807        self.seed_manager.as_mut()
808    }
809
810    /// Check if seeding is active.
811    pub fn is_seeding(&self) -> bool {
812        self.seed_manager.is_some()
813    }
814
815    /// Get seeding statistics.
816    ///
817    /// Returns `None` if not seeding.
818    pub fn get_seed_stats(&self) -> Option<SeedStats> {
819        self.seed_manager.as_ref().map(|mgr| {
820            let (total_uploaded, upload_speed) = mgr.get_upload_stats();
821            let total_downloaded = mgr.total_downloaded();
822            let ratio = if total_downloaded > 0 {
823                total_uploaded as f64 / total_downloaded as f64
824            } else {
825                0.0
826            };
827
828            SeedStats {
829                total_uploaded,
830                upload_speed,
831                ratio,
832                elapsed: mgr.seeding_duration(),
833            }
834        })
835    }
836}
837
838/// Seeding statistics for a completed download.
839#[derive(Debug, Clone)]
840pub struct SeedStats {
841    /// Total bytes uploaded during seeding
842    pub total_uploaded: u64,
843    /// Current upload speed in bytes/sec
844    pub upload_speed: u64,
845    /// Upload/download ratio
846    pub ratio: f64,
847    /// Time elapsed since seeding started
848    pub elapsed: std::time::Duration,
849}